diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8b8f244
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.project
+.DS_Store
+Thumbs.db
+*.php~
+__pycache__
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8cdb845
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ {description}
+ Copyright (C) {year} {fullname}
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ {signature of Ty Coon}, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b79d601
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# plugin-googlecast
diff --git a/core/class/googlecast.class.php b/core/class/googlecast.class.php
new file mode 100644
index 0000000..55d45ec
--- /dev/null
+++ b/core/class/googlecast.class.php
@@ -0,0 +1,749 @@
+.
+ */
+
+/* * ***************************Includes********************************* */
+
+class googlecast extends eqLogic {
+ /* * *************************Attributs****************************** */
+
+ private $_collectDate = '';
+ public static $_widgetPossibility = array('custom' => true);
+
+ /* * ***********************Methode static*************************** */
+
+
+ /* * *********************Methode d'instance************************* */
+
+ public function preUpdate() {
+ /*
+ if ($this->getConfiguration('socketport') == '') {
+ throw new Exception(__('Le champs Port Socket ne peut etre vide', __FILE__));
+ }
+ */
+ }
+
+ public function preRemove() {
+ $this->disallowDevice();
+ }
+
+
+ public function postSave() {
+ $order = 1;
+
+ $cmd = $this->getCmd(null, 'refresh');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('refresh');
+ $cmd->setName(__('Rafraîchir', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setConfiguration('googlecast_cmd', true);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'online');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('online');
+ $cmd->setIsVisible(1);
+ $cmd->setName(__('Online', __FILE__));
+ $cmd->setTemplate('dashboard', 'googlecast_status');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ }
+ $cmd->setType('info');
+ $cmd->setSubType('binary');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->setDisplay('generic_type', 'ENERGY_STATE');
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'reboot');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('reboot');
+ $cmd->setName(__('Restart', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setTemplate('dashboard', 'googlecast_reboot');
+ $cmd->setDisplay('icon', ' ');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'is_busy');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('is_busy');
+ $cmd->setIsVisible(1);
+ $cmd->setName(__('Occupé', __FILE__));
+ $cmd->setTemplate('dashboard', 'googlecast_busy');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ }
+ $cmd->setType('info');
+ $cmd->setSubType('binary');
+
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->setDisplay('generic_type', 'ENERGY_STATE');
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'volume_level');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('volume_level');
+ $cmd->setIsVisible(0);
+ $cmd->setName(__('Volume', __FILE__));
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('info');
+ $cmd->setSubType('numeric');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->setUnite('%');
+ $cmd->setDisplay('generic_type', 'LIGHT_STATE');
+ $cmd->save();
+ $volume_id = $cmd->getId();
+
+ $cmd = $this->getCmd(null, 'volume_set');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('volume_set');
+ $cmd->setName(__('Volume niveau', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('slider');
+ $cmd->setConfiguration('minValue', 0);
+ $cmd->setConfiguration('maxValue', 100);
+ $cmd->setValue($volume_id);
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'volume_muted');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('volume_muted');
+ $cmd->setIsVisible(0);
+ $cmd->setName(__('Mute', __FILE__));
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('info');
+ $cmd->setSubType('binary');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->setDisplay('generic_type', 'SIREN_STATE');
+ $cmd->save();
+ $mute_id = $cmd->getId();
+
+ $cmd = $this->getCmd(null, 'volume_down');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('volume_down');
+ $cmd->setName(__('Volume -', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setDisplay('icon', ' ');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'volume_up');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('volume_up');
+ $cmd->setName(__('Volume +', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setDisplay('icon', ' ');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'mute_on');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('mute_on');
+ $cmd->setName(__('Muet ON', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setTemplate('dashboard', 'btnCircle');
+ $cmd->setTemplate('mobile', 'binaryDefault');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->setDisplay('generic_type', 'SIREN_ON');
+ $cmd->setValue($mute_id);
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'mute_off');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('mute_off');
+ $cmd->setName(__('Muet OFF', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setTemplate('dashboard', 'btnCircle');
+ $cmd->setTemplate('mobile', 'binaryDefault');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->setDisplay('generic_type', 'SIREN_OFF');
+ $cmd->setValue($mute_id);
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'status_text');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('status_text');
+ $cmd->setIsVisible(1);
+ $cmd->setName(__('Statut', __FILE__));
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setDisplay('showNameOndashboard', false);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('info');
+ $cmd->setSubType('string');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->setDisplay('generic_type', 'GENERIC');
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'player_state');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('player_state');
+ $cmd->setIsVisible(0);
+ $cmd->setName(__('Statut Player', __FILE__));
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setDisplay('showNameOndashboard', false);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('info');
+ $cmd->setSubType('string');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->setDisplay('generic_type', 'GENERIC');
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'quit_app');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('quit_app');
+ $cmd->setName(__('Quitter', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setDisplay('icon', ' ');
+ #$cmd->setDisplay('forceReturnLineBefore', 1);
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'stop');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('stop');
+ $cmd->setName(__('Stop', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setDisplay('icon', ' ');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'pause');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('pause');
+ $cmd->setName(__('Pause', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setDisplay('icon', ' ');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'rewind');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('rewind');
+ $cmd->setName(__('Back', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setDisplay('icon', ' ');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'skip');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('skip');
+ $cmd->setName(__('Skip', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setDisplay('icon', ' ');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'play');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('play');
+ $cmd->setName(__('Play', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setDisplay('icon', ' ');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'nowplaying');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('nowplaying');
+ $cmd->setName(__('Playing Widget', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setTemplate('dashboard','googlecast_playing');
+ $cmd->setConfiguration('googlecast_cmd', true);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('info');
+ $cmd->setSubType('string');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $cmd = $this->getCmd(null, 'customcmd');
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId('customcmd');
+ $cmd->setName(__('Custom Cmd', __FILE__));
+ $cmd->setIsVisible(0);
+ $cmd->setOrder($order++);
+ $cmd->setConfiguration('googlecast_cmd', true);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('message');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ if ($this->getConfiguration('firstTimeCreation', True)) {
+
+ $logid = "app=backdrop";
+ $cmd = $this->getCmd(null, $logid);
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId($logid);
+ $cmd->setName(__('Backdrop', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setOrder(200);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $logid = "app=youtube|cmd=play_video|value=fra4QBLF3GU";
+ $cmd = $this->getCmd(null, $logid);
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId($logid);
+ $cmd->setName(__('YouTube', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setOrder(200);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $logid = "app=media|cmd=play_media|value='http://bit.ly/2JzYtfX,video/mp4','Mon film'";
+ $cmd = $this->getCmd(null, $logid);
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId($logid);
+ $cmd->setName(__('Media', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setOrder(201);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $logid = "app=web|cmd=load_url|value='http://pictoplasma.sound-creatures.com',True,10";
+ $cmd = $this->getCmd(null, $logid);
+ if (!is_object($cmd)) {
+ $cmd = new googlecastCmd();
+ $cmd->setLogicalId($logid);
+ $cmd->setName(__('Web', __FILE__));
+ $cmd->setIsVisible(1);
+ $cmd->setOrder(202);
+ $cmd->setOrder($order++);
+ }
+ $cmd->setType('action');
+ $cmd->setSubType('other');
+ $cmd->setEqLogic_id($this->getId());
+ $cmd->save();
+
+ $this->setConfiguration('firstTimeCreation', False);
+ $this->save();
+ }
+
+ $this->checkAndUpdateCmd('nowplaying', $this->getLogicalId());
+
+ $this->allowDevice();
+ }
+
+ public static function createFromDef($_def) {
+ event::add('jeedom::alert', array(
+ 'level' => 'warning',
+ 'page' => 'googlecast',
+ 'message' => __('Nouveau GoogleCast detecté', __FILE__),
+ ));
+ if (!isset($_def['uuid']) || !isset($_def['def'])) {
+ log::add('googlecast', 'error', 'Information manquante pour ajouter l\'équipement : ' . print_r($_def, true));
+ event::add('jeedom::alert', array(
+ 'level' => 'danger',
+ 'page' => 'googlecast',
+ 'message' => __('Information manquante pour ajouter l\'équipement. Inclusion impossible', __FILE__),
+ ));
+ return false;
+ }
+
+ $googlecast = googlecast::byLogicalId($_def['uuid'], 'googlecast');
+ if (!is_object($googlecast)) {
+ $eqLogic = new googlecast();
+ $eqLogic->setName($_def['friendly_name']);
+ }
+ $eqLogic->setLogicalId($_def['uuid']);
+ $eqLogic->setEqType_name('googlecast');
+ $eqLogic->setIsEnable(1);
+ $eqLogic->setIsVisible(1);
+ $eqLogic->setConfiguration('device', $_def['def']['cast_type']);
+ $eqLogic->setConfiguration('friendly_name', $_def['friendly_name']);
+ $eqLogic->setConfiguration('model_name', $_def['def']['model_name']);
+ $eqLogic->setConfiguration('manufacturer', $_def['def']['manufacturer']);
+ $eqLogic->setConfiguration('cast_type', $_def['def']['cast_type']);
+ $eqLogic->setConfiguration('cancontrol',1);
+ $eqLogic->setConfiguration('needsrefresh',0);
+ $eqLogic->setConfiguration('specificwidgets',0);
+ #$eqLogic->setConfiguration('iconModel', 'niu/niu_' . strtolower($_def['color']));
+
+ $eqLogic->save();
+
+
+
+ event::add('jeedom::alert', array(
+ 'level' => 'warning',
+ 'page' => 'googlecast',
+ 'message' => __('Module inclu avec succès ' .$_def['friendly_name'], __FILE__),
+ ));
+ return $eqLogic;
+ }
+
+ /* * **********************Getteur Setteur*************************** */
+
+ public static function deamon_info() {
+ $return = array();
+ $return['log'] = 'googlecast';
+ $return['state'] = 'nok';
+ $pid_file = '/tmp/googlecast.pid';
+ if (file_exists($pid_file)) {
+ if (@posix_getsid(trim(file_get_contents($pid_file)))) {
+ $return['state'] = 'ok';
+ } else {
+ shell_exec('sudo rm -rf ' . $pid_file . ' 2>&1 > /dev/null;rm -rf ' . $pid_file . ' 2>&1 > /dev/null;');
+ }
+ }
+ $return['launchable'] = 'ok';
+ $socketport = config::byKey('socketport', 'googlecast');
+ if ($socketport == '') {
+ $return['launchable'] = 'nok';
+ $return['launchable_message'] = __('Le port n\'est pas configuré', __FILE__);
+ }
+ return $return;
+ }
+
+ public static function dependancy_info() {
+ $return = array();
+ $return['log'] = 'googlecast_update';
+ $return['progress_file'] = '/tmp/dependancy_googlecast_in_progress';
+ if (exec('sudo pip list | grep -E "bluepy" | wc -l') < 1) {
+ $return['state'] = 'nok';
+ } else {
+ $return['state'] = 'ok';
+ }
+ return $return;
+ }
+
+ public static function dependancy_install() {
+ log::remove('googlecast_update');
+ $cmd = 'sudo /bin/bash ' . dirname(__FILE__) . '/../../resources/install.sh';
+ $cmd .= ' >> ' . log::getPathToLog('googlecast_dependancy') . ' 2>&1 &';
+ exec($cmd);
+ }
+
+ public static function deamon_start() {
+ self::deamon_stop();
+ $deamon_info = self::deamon_info();
+ if ($deamon_info['launchable'] != 'ok') {
+ throw new Exception(__('Veuillez vérifier la configuration', __FILE__));
+ }
+ $port = "hey";
+ $googlecast_path = realpath(dirname(__FILE__) . '/../../resources');
+ $cmd = 'sudo /usr/bin/python3 ' . $googlecast_path . '/googlecast.py';
+ $cmd .= ' --loglevel ' . log::convertLogLevel(log::getLogLevel('googlecast'));
+ $cmd .= ' --device ' . $port;
+ $cmd .= ' --socketport ' . config::byKey('socketport', 'googlecast');
+ $cmd .= ' --sockethost 127.0.0.1';
+ $cmd .= ' --callback ' . network::getNetworkAccess('internal', 'proto:127.0.0.1:port:comp') . '/plugins/googlecast/core/php/googlecast.api.php';
+ $cmd .= ' --apikey ' . jeedom::getApiKey('googlecast');
+ $cmd .= ' --daemonname local';
+ log::add('googlecast', 'info', 'Lancement démon googlecast : ' . $cmd);
+ $result = exec($cmd . ' >> ' . log::getPathToLog('googlecast_local') . ' 2>&1 &');
+ $i = 0;
+ while ($i < 30) {
+ $deamon_info = self::deamon_info();
+ if ($deamon_info['state'] == 'ok') {
+ break;
+ }
+ sleep(1);
+ $i++;
+ }
+ if ($i >= 30) {
+ log::add('googlecast', 'error', __('Impossible de lancer le démon googlecast, vérifiez la log',__FILE__), 'unableStartDeamon');
+ return false;
+ }
+ message::removeAll('googlecast', 'unableStartDeamon');
+ config::save('include_mode', 0, 'googlecast');
+ return true;
+ }
+
+
+ public static function socket_connection($_value) {
+ $socket = socket_create(AF_INET, SOCK_STREAM, 0);
+ socket_connect($socket, '127.0.0.1', config::byKey('socketport', 'googlecast'));
+ socket_write($socket, $_value, strlen($_value));
+ socket_close($socket);
+ }
+
+ public static function changeLogLive($_level) {
+ $value = array('apikey' => jeedom::getApiKey('googlecast'), 'cmd' => $_level);
+ $value = json_encode($value);
+ self::socket_connection($value,True);
+ }
+
+ public static function deamon_stop() {
+ $pid_file = '/tmp/googlecast.pid';
+ if (file_exists($pid_file)) {
+ $pid = intval(trim(file_get_contents($pid_file)));
+ system::kill($pid);
+ }
+ system::kill('googlecast.py');
+ system::fuserk(config::byKey('socketport', 'googlecast'));
+ sleep(1);
+ }
+
+ public static function changeIncludeState($_state, $_mode) {
+ if ($_mode == 1) {
+ if ($_state == 1) {
+ $value = json_encode(array('apikey' => jeedom::getApiKey('googlecast'), 'cmd' => 'learnin'));
+ self::socket_connection($value,True);
+ } else {
+ $value = json_encode(array('apikey' => jeedom::getApiKey('googlecast'), 'cmd' => 'learnout'));
+ self::socket_connection($value,True);
+ }
+ }
+ }
+
+ public static function sendIdToDeamon() {
+ foreach (self::byType('googlecast') as $eqLogic) {
+ $eqLogic->allowDevice();
+ usleep(500);
+ }
+ }
+
+
+ public function allowDevice() {
+ $value = array('apikey' => jeedom::getApiKey('googlecast'), 'cmd' => 'add');
+
+ if ($this->getLogicalId() != '') {
+ $value['device'] = array(
+ 'uuid' => $this->getLogicalId(),
+ 'name' => $this->getConfiguration('friendly_name','Unknown')
+ );
+ $value = json_encode($value);
+ self::socket_connection($value,True);
+ }
+ }
+
+
+ public static function registerNowPlayging($uuid) {
+ #$googlecast = googlecast::byLogicalId($uuid, 'googlecast');
+ #if (!is_object($googlecast)) {
+ $value = array('apikey' => jeedom::getApiKey('googlecast'), 'cmd' => 'nowplaying', 'uuid' => $uuid);
+ $value = json_encode($value);
+ self::socket_connection($value,True);
+ #}
+ }
+/*
+ public function followRealtime() {
+ $value = array('apikey' => jeedom::getApiKey('googlecast'), 'cmd' => 'realtime');
+
+ if ($this->getLogicalId() != '') {
+ $value['device'] = array(
+ 'uuid' => $this->getLogicalId(),
+ 'name' => $this->getConfiguration('friendly_name','Unknown')
+ );
+ $value = json_encode($value);
+ self::socket_connection($value,True);
+ }
+ }
+*/
+ public function disallowDevice() {
+ if ($this->getLogicalId() == '') {
+ return;
+ }
+ $value = json_encode(array('apikey' => jeedom::getApiKey('googlecast'), 'cmd' => 'remove', 'device' => array('uuid' => $this->getLogicalId())));
+ self::socket_connection($value,True);
+ }
+
+ public function getCurrentPlaying($id) {
+ $eqLogic = eqLogic::byId($id);
+ if ($eqLogic) {
+ $cmd = $eqLogic->getCmd(null, 'nowplaying');
+ if (!is_object($cmd)) {
+ $cmd->getCache('nowplaying');
+ }
+ return $eqLogic->getDisplayData();
+ }
+ return 'Error fetching '.$id;
+ }
+
+}
+
+class googlecastcmd extends cmd {
+ /* * *************************Attributs****************************** */
+
+ /* * ***********************Methode static*************************** */
+
+ /* * *********************Methode d'instance************************* */
+
+ public function execute($_options = null) {
+ if ($this->getType() != 'action') {
+ return;
+ }
+ $eqLogic = $this->getEqLogic();
+ $listCmd = $this->getLogicalId();
+ # special case of custom command
+ if ($this->getLogicalId() == "customcmd") {
+ $listCmd = trim($_options['message']);
+ }
+ $values = explode('|', $listCmd);
+ foreach ($values as $value) {
+ $value = explode('=', $value);
+ if (count($value) == 2) {
+ switch ($this->getSubType()) {
+ case 'slider':
+ $data[trim($value[0])] = trim(str_replace('#slider#', $_options['slider'], $value[1]));
+ break;
+ case 'color':
+ $data[trim($value[0])] = str_replace('#','',trim(str_replace('#color#', $_options['color'], $value[1])));
+ break;
+ case 'select':
+ $data[trim($value[0])] = trim(str_replace('#listValue#', $_options['select'], $value[1]));
+ break;
+ case 'message':
+ $data[trim($value[0])] = trim(str_replace('#message#', $_options['message'], $value[1]));
+ $data[trim($value[0])] = trim(str_replace('#title#', $_options['title'], $data[trim($value[0])]));
+ break;
+ default:
+ $data[trim($value[0])] = trim($value[1]);
+ }
+ }
+ elseif (count($value) == 1) {
+ $data['cmd'] = trim($value[0]);
+ switch ($this->getSubType()) {
+ case 'slider':
+ $data['value'] = $_options['slider'];
+ break;
+ case 'select':
+ $data['value'] = trim($_options['select']);
+ break;
+ case 'message':
+ $data['value'] = trim($_options['message']);
+ break;
+ }
+ }
+ }
+ $data['device'] = array(
+ 'uuid' => $eqLogic->getLogicalId(),
+ 'delay' => $eqLogic->getConfiguration('delay',0),
+ 'needsrefresh' => $eqLogic->getConfiguration('needsrefresh',0),
+ 'name' => $eqLogic->getConfiguration('name','0'),
+ );
+ if (count($data) == 0) {
+ return;
+ }
+ if ($this->getLogicalId() == 'refresh'){
+ $data['name'] = $eqLogic->getConfiguration('name','0');
+ $value = json_encode(array('apikey' => jeedom::getApiKey('googlecast'), 'cmd' => $this->getLogicalId(), 'device' => array('uuid' => $eqLogic->getLogicalId()), 'command' => $data));
+ } else {
+ $value = json_encode(array('apikey' => jeedom::getApiKey('googlecast'), 'cmd' => 'action', 'device' => array('uuid' => $eqLogic->getLogicalId()), 'command' => $data));
+ }
+ log::add('googlecast','debug',"Envoi d'une commande depuis Jeedom");
+ googlecast::socket_connection($value);
+
+ }
+
+ /* * **********************Getteur Setteur*************************** */
+}
diff --git a/core/php/googlecast.ajax.php b/core/php/googlecast.ajax.php
new file mode 100644
index 0000000..02758a7
--- /dev/null
+++ b/core/php/googlecast.ajax.php
@@ -0,0 +1,93 @@
+.
+ */
+try {
+ require_once dirname(__FILE__) . '/../../../../core/php/core.inc.php';
+ //require_once dirname(__FILE__) . '/../class/googlecast.class.php';
+
+ include_file('core', 'authentification', 'php');
+
+ ajax::init();
+
+ if (init('action') == 'changeIncludeState') {
+ googlecast::changeIncludeState(init('state'), init('mode'));
+ ajax::success();
+ }
+
+ if (init('action') == 'changeLogLive') {
+ ajax::success(googlecast::changeLogLive(init('level')));
+ }
+
+ if (init('action') == 'nowplaying') {
+ ajax::success(googlecast::registerNowPlayging(init('uuid')));
+ }
+
+/*
+ if (init('action') == 'getMobileHealth') {
+ ajax::success(blea::getMobileHealth());
+ }
+
+ if (init('action') == 'loadplaying') {
+ $request_http = new com_http(init('url'));
+ try {
+ $ret = $request_http->exec(1);
+ if ($ret && strlen($ret)>0) {
+ ajax::success(base64_encode ($ret));
+ }
+ else {
+ ajax::error();
+ }
+ } catch (Exception $e) {
+ ajax::error();
+ }
+ }
+*/
+/*
+ if (init('action') == 'loaddisplay') {
+ $jsondata = googlecast::getAjaxDisplayData(init('equid'));
+ if ($jsondata && strlen($jsondata)>2) {
+ ajax::success($jsondata);
+ }
+ else {
+ ajax::error();
+ }
+ }
+
+ if (init('action') == 'sendcmd') {
+ $ret = googlecast::sendDisplayAction(init('equid'),init('cmd'));
+ if ($ret) {
+ ajax::success();
+ }
+ else {
+ ajax::error();
+ }
+ }
+
+ if (init('action') == 'getCmd') {
+ $jsondata = googlecast::getDisplayCommandList(init('equid'));
+ if ($jsondata && strlen($jsondata)>0) {
+ ajax::success($jsondata);
+ }
+ else {
+ ajax::error();
+ }
+ }
+*/
+ //throw new Exception(__('Aucune methode correspondante à : ', __FILE__) . init('action'));
+ /* * *********Catch exeption*************** */
+} catch (Exception $e) {
+ ajax::error(displayExeption($e), $e->getCode());
+}
diff --git a/core/php/googlecast.api.php b/core/php/googlecast.api.php
new file mode 100644
index 0000000..c054198
--- /dev/null
+++ b/core/php/googlecast.api.php
@@ -0,0 +1,112 @@
+.
+ */
+require_once dirname(__FILE__) . "/../../../../core/php/core.inc.php";
+
+if (!jeedom::apiAccess(init('apikey'), 'googlecast')) {
+ echo 'Clef API non valide, vous n\'etes pas autorisé à effectuer cette action';
+ die();
+}
+
+if (init('test') != '') {
+ echo 'OK';
+ die();
+}
+$result = json_decode(file_get_contents("php://input"), true);
+if (!is_array($result)) {
+ die();
+}
+if (isset($result['source'])){
+ log::add('googlecast','debug','This is a message from googlecast program ' . $result['source']);
+}
+
+if (isset($result['learn_mode'])) {
+ if ($result['learn_mode'] == 1) {
+ config::save('include_mode', 1, 'googlecast');
+ event::add('googlecast::includeState', array(
+ 'mode' => 'learn',
+ 'state' => 1)
+ );
+ } else {
+ config::save('include_mode', 0, 'googlecast');
+ event::add('googlecast::includeState', array(
+ 'mode' => 'learn',
+ 'state' => 0)
+ );
+ }
+}
+
+if (isset($result['started'])) {
+ if ($result['started'] == 1) {
+ log::add('googlecast','info','Process started. Sending known devices now...');
+ usleep(500);
+ googlecast::sendIdToDeamon();
+ }
+}
+if (isset($result['heartbeat'])) {
+ if ($result['heartbeat'] == 1) {
+ log::add('googlecast','info','Googlecast program heartbeat');
+ }
+}
+
+if ( isset($result['nowplaying']) ) {
+ if ( isset($result['uuid']) ) {
+ event::add('googlecast::'.$result['uuid'].'::nowplaying', $result);
+ }
+}
+
+if (isset($result['devices'])) {
+
+ foreach ($result['devices'] as $key => $data) {
+ if (!isset($data['uuid'])) {
+ continue;
+ }
+ $googlecast = googlecast::byLogicalId($data['uuid'], 'googlecast');
+ if (!is_object($googlecast)) {
+ if ($data['learn'] != 1) {
+ continue;
+ }
+ log::add('googlecast','info','New GoogleCast device detected ' . $data['uuid']);
+ $googlecast = googlecast::createFromDef($data);
+ event::add('jeedom::alert', array(
+ 'level' => 'warning',
+ 'page' => 'googlecast',
+ 'message' => '',
+ ));
+ event::add('googlecast::includeDevice', $googlecast->getId());
+ }
+
+ foreach ($googlecast->getCmd('info') as $cmd) {
+ $logicalId = $cmd->getLogicalId();
+ $result = array_flatten($data);
+ if ( isset($result[$logicalId]) ) {
+ $cmd->event($result[$logicalId]);
+ }
+ }
+ }
+}
+
+function array_flatten($array) {
+ $return = array();
+ foreach ($array as $key => $value) {
+ if (is_array($value))
+ $return = array_merge($return, array_flatten($value));
+ else
+ $return[$key] = $value;
+ }
+ return $return;
+}
diff --git a/core/template/dashboard/cmd.action.other.googlecast_reboot.html b/core/template/dashboard/cmd.action.other.googlecast_reboot.html
new file mode 100644
index 0000000..0cb1f01
--- /dev/null
+++ b/core/template/dashboard/cmd.action.other.googlecast_reboot.html
@@ -0,0 +1,10 @@
+
+#name_display#
+
+
diff --git a/core/template/dashboard/cmd.info.binary.googlecast_busy.html b/core/template/dashboard/cmd.info.binary.googlecast_busy.html
new file mode 100644
index 0000000..38f0f8c
--- /dev/null
+++ b/core/template/dashboard/cmd.info.binary.googlecast_busy.html
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/core/template/dashboard/cmd.info.binary.googlecast_status.html b/core/template/dashboard/cmd.info.binary.googlecast_status.html
new file mode 100644
index 0000000..b873aed
--- /dev/null
+++ b/core/template/dashboard/cmd.info.binary.googlecast_status.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/core/template/dashboard/cmd.info.string.googlecast_playing.html b/core/template/dashboard/cmd.info.string.googlecast_playing.html
new file mode 100644
index 0000000..fdfef9b
--- /dev/null
+++ b/core/template/dashboard/cmd.info.string.googlecast_playing.html
@@ -0,0 +1,110 @@
+
+
diff --git a/desktop/images/chromecast1.png b/desktop/images/chromecast1.png
new file mode 100644
index 0000000..df80a1e
Binary files /dev/null and b/desktop/images/chromecast1.png differ
diff --git a/desktop/images/chromecast2.png b/desktop/images/chromecast2.png
new file mode 100644
index 0000000..0d5c30d
Binary files /dev/null and b/desktop/images/chromecast2.png differ
diff --git a/desktop/js/googlecast.js b/desktop/js/googlecast.js
new file mode 100644
index 0000000..8fda94d
--- /dev/null
+++ b/desktop/js/googlecast.js
@@ -0,0 +1,196 @@
+
+/* This file is part of Jeedom.
+ *
+ * Jeedom is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Jeedom is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Jeedom. If not, see .
+ */
+
+ $('.changeIncludeState').on('click', function () {
+ var mode = $(this).attr('data-mode');
+ var state = $(this).attr('data-state');
+ changeIncludeState(state, mode);
+});
+
+ $('#bt_healthgooglecast').on('click', function () {
+ $('#md_modal').dialog({title: "{{Santé GoogleCast}}"});
+ $('#md_modal').load('index.php?v=d&plugin=googlecast&modal=health').dialog('open');
+});
+
+$('body').on('googlecast::includeState', function (_event,_options) {
+ if (_options['mode'] == 'learn') {
+ if (_options['state'] == 1) {
+ if($('.include').attr('data-state') != 0){
+ $.hideAlert();
+ $('.include:not(.card)').removeClass('btn-default').addClass('btn-success');
+ $('.include').attr('data-state', 0);
+ //$('.include.card').css('background-color','#8000FF');
+ $('.include.card span center').text('{{Arrêter le scan}}');
+ $('.includeicon').empty().append(' ');
+ $('.includeicon_text').css('color', 'red').css('font-weight', 'bold');
+ $('#div_inclusionAlert').showAlert({message: '{{Mode scan en cours pendant 1 minute... (Cliquer sur arrêter pour stopper avant)}}', level: 'warning'});
+ }
+ } else {
+ if($('.include').attr('data-state') != 1){
+ $.hideAlert();
+ $('.include:not(.card)').addClass('btn-default').removeClass('btn-success btn-danger');
+ $('.include').attr('data-state', 1);
+ $('.includeicon').empty().append(' ');
+ $('.includeicon_text').css('color', '#94ca02').css('font-weight', 'normal');
+ $('.include.card span center').text('{{Lancer Scan}}');
+ $('.include.card').css('background-color','#ffffff');
+ }
+ }
+ }
+});
+
+$('body').on('googlecast::includeDevice', function (_event,_options) {
+ if (modifyWithoutSave) {
+ $('#div_inclusionAlert').showAlert({message: '{{Un GoogleCast vient d\'être inclu/exclu. Veuillez réactualiser la page}}', level: 'warning'});
+ } else {
+ if (_options == '') {
+ window.location.reload();
+ } else {
+ window.location.href = 'index.php?v=d&p=googlecast&m=googlecast&id=' + _options;
+ }
+ }
+});
+
+function changeIncludeState(_state,_mode,_type='') {
+ $.ajax({// fonction permettant de faire de l'ajax
+ type: "POST", // methode de transmission des données au fichier php
+ url: "plugins/googlecast/core/php/googlecast.ajax.php", // url du fichier php
+ data: {
+ action: "changeIncludeState",
+ state: _state,
+ mode: _mode,
+ },
+ dataType: 'json',
+ error: function (request, status, error) {
+ handleAjaxError(request, status, error);
+ },
+ success: function (data) { // si l'appel a bien fonctionné
+ if (data.state != 'ok') {
+ $('#div_alert').showAlert({message: data.result, level: 'danger'});
+ return;
+ }
+ }
+ });
+}
+
+
+$("#table_cmd").sortable({axis: "y", cursor: "move", items: ".cmd", placeholder: "ui-state-highlight", tolerance: "intersect", forcePlaceholderSize: true});
+
+
+function addCmdToTable(_cmd) {
+ if (!isset(_cmd)) {
+ var _cmd = {configuration: {}};
+ }
+ var tr = '';
+ if ( (_cmd.logicalId != null) && _cmd.configuration.googlecast_cmd!==undefined ) {
+ tr += '';
+ tr += '';
+ tr += '
';
+ tr += '
Icône';
+ tr += '
';
+ tr += '
';
+ tr += '
';
+ tr += ' ';
+ tr += '
';
+ tr += '
';
+ tr += '';
+ tr += 'Aucune ';
+ tr += ' ';
+ tr += ' ';
+ tr += '';
+ tr += ' ';
+ tr += '' + init(_cmd.type) + ' ';
+ tr += ' ';
+ tr += ' ';
+ tr += ' ';
+ tr += '';
+ tr += ' {{Afficher}} ';
+ tr += ' {{Historiser}} ';
+ tr += ' ';
+ tr += '';
+ tr += ' ';
+ tr += ' ';
+ tr += '';
+ if (is_numeric(_cmd.id)) {
+ tr += ' ';
+ tr += ' Tester ';
+ }
+ //tr += ' ';
+} else { // is new created command
+ tr += '';
+ tr += '';
+ tr += '
';
+ tr += '
Icône';
+ tr += '
';
+ tr += '
';
+ tr += '
';
+ tr += ' ';
+ tr += '
';
+ tr += '
';
+ tr += '';
+ tr += 'Aucune ';
+ tr += ' ';
+ tr += ' ';
+ tr += '';
+ tr += ' ';
+ tr += '' + jeedom.cmd.availableType() + ' ';
+ tr += ' ';
+ tr += ' ';
+ tr += ' ';
+ tr += ' ';
+ tr += '';
+ tr += ' {{Afficher}} ';
+ tr += ' {{Historiser}} ';
+ tr += ' {{Inverser}} ';
+ tr += ' ';
+ tr += ' ';
+ tr += ' ';
+ tr += '';
+ tr += '';
+ tr += 'Aucune ';
+ tr += ' ';
+ tr += ' ';
+ tr += ' ';
+ tr += ' ';
+ tr += ' ';
+ tr += ' ';
+ tr += '';
+ if (is_numeric(_cmd.id)) {
+ tr += ' ';
+ tr += ' Tester ';
+ }
+ if (_cmd.configuration.googlecast_cmd_mod===undefined) {
+ tr += ' ';
+ }
+}
+ tr += ' ';
+ $('#table_cmd tbody').append(tr);
+ var tr = $('#table_cmd tbody tr:last');
+ jeedom.eqLogic.builSelectCmd({
+ id: $(".li_eqLogic.active").attr('data-eqLogic_id'),
+ filter: {type: 'info'},
+ error: function (error) {
+ $('#div_alert').showAlert({message: error.message, level: 'danger'});
+ },
+ success: function (result) {
+ tr.find('.cmdAttr[data-l1key=value]').append(result);
+ tr.find('.cmdAttr[data-l1key=configuration][data-l2key=updateCmdId]').append(result);
+ tr.setValues(_cmd, '.cmdAttr');
+ jeedom.cmd.changeType(tr, init(_cmd.subType));
+ }
+ });
+}
diff --git a/desktop/php/googlecast.php b/desktop/php/googlecast.php
new file mode 100644
index 0000000..f3b3a72
--- /dev/null
+++ b/desktop/php/googlecast.php
@@ -0,0 +1,186 @@
+getId());
+$eqLogics = eqLogic::byType($plugin->getId());
+
+function sortByOption($a, $b) {
+ return strcmp($a['name'], $b['name']);
+}
+if (config::byKey('include_mode', 'googlecast', 0) == 1) {
+ echo '{{Vous êtes en mode scan. Recliquez sur le bouton scan pour sortir de ce mode (sinon le mode restera actif une minute)}}
';
+} else {
+ echo '
';
+}
+
+?>
+
+
+
+
+
+
{{Gestion}}
+
+ ';
+ echo '
';
+ echo ' ';
+ echo ' ';
+ echo '{{Arrêter Scan}} ';
+ echo '';
+} else {
+ echo '
';
+ echo '
';
+ echo ' ';
+ echo ' ';
+ echo '{{Lancer Scan}} ';
+ echo '';
+}
+?>
+
+
+
+
+ {{Configuration}}
+
+
+
{{Mes équipements GoogleCast}}
+
+
+ getIsEnable() != 1) {
+ $opacity = 'opacity:0.3;';
+ }
+ echo '
';
+ echo '
';
+ echo "
";
+ echo '
' . $eqLogic->getHumanName(true, true) . ' ';
+ echo '
';
+ $url = network::getNetworkAccess('external') . '/plugins/googlecast/core/php/googlecast.ajax.php?apikey=' . jeedom::getApiKey('googlecast') . '&id=' . $eqLogic->getId();
+}
+?>
+
+
+
+
+
+
+
diff --git a/docs/_config.yml b/docs/_config.yml
new file mode 100644
index 0000000..0a63f24
--- /dev/null
+++ b/docs/_config.yml
@@ -0,0 +1,15 @@
+---
+langs: [fr_FR,en_US,de_DE,es_ES]
+baseurl: "/"
+sass:
+ sass_dir: assets/css
+ style: compressed
+markdown: kramdown
+kramdown:
+ input: GFM
+ hard_wrap: false
+highlighter: rouge
+gems:
+- jekyll-paginate
+exclude:
+- vendor
diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html
new file mode 100644
index 0000000..e35bef8
--- /dev/null
+++ b/docs/_layouts/default.html
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ language
+
+
+
+ {% if site.langs.size >= 2 %}
+ {% for lang in site.langs %}
+ {% if lang == "fr_FR" %}Français{% endif %}{% if lang == "en_US" %}English{% endif %}{% if lang == "ru_RU" %}Russe{% endif %}{% if lang == "de_DE" %}Deutsch{% endif %}{% if lang == "es_ES" %}Spanish{% endif %}{% if lang == "it_IT" %}Italien{% endif %}{% if lang == "id_ID" %}Indien{% endif %}
+ {% endfor %}
+ {% endif %}
+
+
+
+
+
+
+
+
keyboard_arrow_up
+
+
+ {{content}}
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/assets/css/components/_buttons.scss b/docs/assets/css/components/_buttons.scss
new file mode 100644
index 0000000..cb34879
--- /dev/null
+++ b/docs/assets/css/components/_buttons.scss
@@ -0,0 +1,197 @@
+// shared styles
+.btn,
+.btn-flat {
+ border: $button-border;
+ border-radius: $button-radius;
+ display: inline-block;
+ height: $button-height;
+ line-height: $button-height;
+ outline: 0;
+ padding: $button-padding;
+ text-transform: uppercase;
+ vertical-align: middle;
+ // Gets rid of tap active state
+ -webkit-tap-highlight-color: transparent;
+}
+
+// Disabled shared style
+.btn.disabled,
+.btn-floating.disabled,
+.btn-large.disabled,
+.btn:disabled
+.btn-large:disabled,
+.btn-floating:disabled {
+ background-color: $button-disabled-background !important;
+ box-shadow: none;
+ color: $button-disabled-color !important;
+ cursor: default;
+
+ * {
+ pointer-events: none;
+ }
+
+ &:hover {
+ background-color: $button-disabled-background !important;
+ color: $button-disabled-color !important;
+ }
+}
+
+// Shared icon styles
+.btn,
+.btn-floating,
+.btn-large,
+.btn-flat {
+ i {
+ font-size: $button-font-size;
+ line-height: inherit;
+ }
+}
+
+// Raised Button
+.btn {
+ text-decoration: none;
+ color: $button-raised-color;
+ background-color: $button-raised-background;
+ text-align: center;
+ letter-spacing: .5px;
+ @extend .z-depth-1;
+ transition: .2s ease-out;
+ cursor: pointer;
+
+ &:hover {
+ background-color: $button-raised-background-hover;
+ @extend .z-depth-1-half;
+ }
+}
+
+// Floating button
+.btn-floating {
+ display: inline-block;
+ color: $button-floating-color;
+ position: relative;
+ overflow: hidden;
+ z-index: 1;
+ width: $button-floating-size;
+ height: $button-floating-size;
+ line-height: $button-floating-size;
+ padding: 0;
+ background-color: $button-floating-background;
+ border-radius: $button-floating-radius;
+ @extend .z-depth-1;
+ transition: .3s;
+ cursor: pointer;
+ vertical-align: middle;
+
+ i {
+ width: inherit;
+ display: inline-block;
+ text-align: center;
+ color: $button-floating-color;
+ font-size: $button-large-icon-font-size;
+ line-height: $button-floating-size;
+ }
+
+ &:hover {
+ background-color: $button-floating-background-hover;
+ @extend .z-depth-1-half;
+ }
+
+ &:before {
+ border-radius: 0;
+ }
+
+ &.btn-large {
+ width: $button-floating-large-size;
+ height: $button-floating-large-size;
+ i {
+ line-height: $button-floating-large-size;
+ }
+ }
+}
+
+// button fix
+button.btn-floating {
+ border: $button-border;
+}
+
+// Fixed Action Button
+.fixed-action-btn {
+ &.active {
+ ul {
+ visibility: visible;
+ }
+ }
+
+ &.horizontal {
+ padding: 0 0 0 15px;
+
+ ul {
+ text-align: right;
+ right: 64px;
+ top: 50%;
+ transform: translateY(-50%);
+ height: 100%;
+ left: auto;
+ width: 500px; /*width 100% only goes to width of button container */
+
+ li {
+ display: inline-block;
+ margin: 15px 15px 0 0;
+ }
+ }
+ }
+
+ position: fixed;
+ right: 23px;
+ bottom: 23px;
+ padding-top: 15px;
+ margin-bottom: 0;
+ z-index: 998;
+
+ ul {
+ left: 0;
+ right: 0;
+ text-align: center;
+ position: absolute;
+ bottom: 64px;
+ margin: 0;
+ visibility: hidden;
+
+ li {
+ margin-bottom: 15px;
+ }
+
+ a.btn-floating {
+ opacity: 0;
+ }
+ }
+}
+
+// Flat button
+.btn-flat {
+ box-shadow: none;
+ background-color: transparent;
+ color: $button-flat-color;
+ cursor: pointer;
+
+ &.disabled {
+ color: $button-flat-disabled-color;
+ cursor: default;
+ }
+}
+
+// Large button
+.btn-large {
+ @extend .btn;
+ height: $button-large-height;
+ line-height: $button-large-height;
+
+ i {
+ font-size: $button-large-icon-font-size;
+ }
+}
+
+// Block button
+.btn-block {
+ display: block;
+}
diff --git a/docs/assets/css/components/_cards.scss b/docs/assets/css/components/_cards.scss
new file mode 100644
index 0000000..8f5be29
--- /dev/null
+++ b/docs/assets/css/components/_cards.scss
@@ -0,0 +1,138 @@
+
+
+.card-panel {
+ transition: box-shadow .25s;
+ padding: $card-padding;
+ margin: $element-top-margin 0 $element-bottom-margin 0;
+ border-radius: 2px;
+ @extend .z-depth-1;
+ background-color: $card-bg-color;
+}
+
+.card {
+ position: relative;
+ margin: $element-top-margin 0 $element-bottom-margin 0;
+ background-color: $card-bg-color;
+ transition: box-shadow .25s;
+ border-radius: 2px;
+ @extend .z-depth-1;
+
+
+ .card-title {
+ font-size: 24px;
+ font-weight: 300;
+ &.activator {
+ cursor: pointer;
+ }
+ }
+
+ // Card Sizes
+ &.small, &.medium, &.large {
+ position: relative;
+
+ .card-image {
+ max-height: 60%;
+ overflow: hidden;
+ }
+ .card-content {
+ max-height: 40%;
+ overflow: hidden;
+ }
+ .card-action {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+ }
+
+ &.small {
+ height: 300px;
+ }
+
+ &.medium {
+ height: 400px;
+ }
+
+ &.large {
+ height: 500px;
+ }
+
+
+ .card-image {
+ position: relative;
+
+ // Image background for content
+ img {
+ display: block;
+ border-radius: 2px 2px 0 0;
+ position: relative;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ width: 100%;
+ }
+
+ .card-title {
+ color: $card-bg-color;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ padding: $card-padding;
+ }
+
+ }
+
+ .card-content {
+ padding: $card-padding;
+ border-radius: 0 0 2px 2px;
+
+ p {
+ margin: 0;
+ color: inherit;
+ }
+ .card-title {
+ line-height: 48px;
+ }
+ }
+
+ .card-action {
+ position: relative;
+ background-color: inherit;
+ border-top: 1px solid rgba(160,160,160,.2);
+ padding: $card-padding;
+ z-index: 2;
+
+ a:not(.btn):not(.btn-large):not(.btn-floating) {
+ color: $card-link-color;
+ margin-right: $card-padding;
+ transition: color .3s ease;
+ text-transform: uppercase;
+
+ &:hover { color: $card-link-color-light; }
+ }
+
+ & + .card-reveal {
+ z-index: 1;
+ padding-bottom: 64px;
+ }
+ }
+
+ .card-reveal {
+ padding: $card-padding;
+ position: absolute;
+ background-color: $card-bg-color;
+ width: 100%;
+ overflow-y: auto;
+ top: 100%;
+ height: 100%;
+ z-index: 3;
+ display: none;
+
+ .card-title {
+ cursor: pointer;
+ display: block;
+ }
+ }
+}
diff --git a/docs/assets/css/components/_carousel.scss b/docs/assets/css/components/_carousel.scss
new file mode 100644
index 0000000..a5a8acc
--- /dev/null
+++ b/docs/assets/css/components/_carousel.scss
@@ -0,0 +1,34 @@
+.carousel {
+ overflow: hidden;
+ position: relative;
+ width: 100%;
+ height: 400px;
+ perspective: 500px;
+ transform-style: preserve-3d;
+ transform-origin: 0% 50%;
+
+ .carousel-item {
+ width: 200px;
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ img {
+ width: 100%;
+ }
+ }
+
+ &.carousel-slider {
+ top: 0;
+ left: 0;
+ height: 0;
+
+ .carousel-item {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+ }
+}
diff --git a/docs/assets/css/components/_chips.scss b/docs/assets/css/components/_chips.scss
new file mode 100644
index 0000000..316cee6
--- /dev/null
+++ b/docs/assets/css/components/_chips.scss
@@ -0,0 +1,27 @@
+.chip {
+ display: inline-block;
+ height: 32px;
+ font-size: 13px;
+ font-weight: 500;
+ color: rgba(0,0,0,.6);
+ line-height: 32px;
+ padding: 0 12px;
+ border-radius: 16px;
+ background-color: $chip-bg-color;
+
+ img {
+ float: left;
+ margin: 0 8px 0 -12px;
+ height: 32px;
+ width: 32px;
+ border-radius: 50%;
+ }
+
+ i.material-icons {
+ cursor: pointer;
+ float: right;
+ font-size: 16px;
+ line-height: 32px;
+ padding-left: 8px;
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/css/components/_collapsible.scss b/docs/assets/css/components/_collapsible.scss
new file mode 100644
index 0000000..019f811
--- /dev/null
+++ b/docs/assets/css/components/_collapsible.scss
@@ -0,0 +1,90 @@
+.collapsible {
+ border-top: 1px solid $collapsible-border-color;
+ border-right: 1px solid $collapsible-border-color;
+ border-left: 1px solid $collapsible-border-color;
+ margin: $element-top-margin 0 $element-bottom-margin 0;
+ @extend .z-depth-1;
+}
+
+.collapsible-header {
+ display: block;
+ cursor: pointer;
+ min-height: $collapsible-height;
+ line-height: $collapsible-height;
+ padding: 0 1rem;
+ background-color: $collapsible-header-color;
+ border-bottom: 1px solid $collapsible-border-color;
+
+ i {
+ width: 2rem;
+ font-size: 1.6rem;
+ line-height: $collapsible-height;
+ display: block;
+ float: left;
+ text-align: center;
+ margin-right: 1rem;
+ }
+}
+
+.collapsible-body {
+ display: none;
+ border-bottom: 1px solid $collapsible-border-color;
+ box-sizing: border-box;
+
+ p {
+ margin: 0;
+ padding: 2rem;
+ }
+}
+
+// sideNav collapsible styling
+.side-nav,
+.side-nav.fixed {
+
+ .collapsible {
+ border: none;
+ box-shadow: none;
+
+ li { padding: 0; }
+ }
+
+ .collapsible-header {
+ background-color: transparent;
+ border: none;
+ line-height: inherit;
+ height: inherit;
+ padding: 0 $sidenav-padding-right;
+
+ &:hover { background-color: rgba(0,0,0,.05); }
+ i { line-height: inherit; }
+ }
+
+ .collapsible-body {
+ border: 0;
+ background-color: $collapsible-header-color;
+
+ li a {
+ padding: 0 (7.5px + $sidenav-padding-right)
+ 0 (15px + $sidenav-padding-right);
+ }
+ }
+
+}
+
+// Popout Collapsible
+
+.collapsible.popout {
+ border: none;
+ box-shadow: none;
+ > li {
+ box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
+ // transform: scaleX(.92);
+ margin: 0 24px;
+ transition: margin .35s cubic-bezier(0.250, 0.460, 0.450, 0.940);
+ }
+ > li.active {
+ box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
+ margin: 16px 0;
+ // transform: scaleX(1);
+ }
+}
diff --git a/docs/assets/css/components/_color.scss b/docs/assets/css/components/_color.scss
new file mode 100644
index 0000000..8f6996c
--- /dev/null
+++ b/docs/assets/css/components/_color.scss
@@ -0,0 +1,412 @@
+// Utility Color Classes
+
+//.success {
+//
+//}
+
+// Google Color Palette defined: http://www.google.com/design/spec/style/color.html
+
+
+$materialize-red: (
+ "base": #e51c23,
+ "lighten-5": #fdeaeb,
+ "lighten-4": #f8c1c3,
+ "lighten-3": #f3989b,
+ "lighten-2": #ee6e73,
+ "lighten-1": #ea454b,
+ "darken-1": #d0181e,
+ "darken-2": #b9151b,
+ "darken-3": #a21318,
+ "darken-4": #8b1014,
+);
+
+$red: (
+ "base": #F44336,
+ "lighten-5": #FFEBEE,
+ "lighten-4": #FFCDD2,
+ "lighten-3": #EF9A9A,
+ "lighten-2": #E57373,
+ "lighten-1": #EF5350,
+ "darken-1": #E53935,
+ "darken-2": #D32F2F,
+ "darken-3": #C62828,
+ "darken-4": #B71C1C,
+ "accent-1": #FF8A80,
+ "accent-2": #FF5252,
+ "accent-3": #FF1744,
+ "accent-4": #D50000
+);
+
+$pink: (
+ "base": #e91e63,
+ "lighten-5": #fce4ec,
+ "lighten-4": #f8bbd0,
+ "lighten-3": #f48fb1,
+ "lighten-2": #f06292,
+ "lighten-1": #ec407a,
+ "darken-1": #d81b60,
+ "darken-2": #c2185b,
+ "darken-3": #ad1457,
+ "darken-4": #880e4f,
+ "accent-1": #ff80ab,
+ "accent-2": #ff4081,
+ "accent-3": #f50057,
+ "accent-4": #c51162
+);
+
+$purple: (
+ "base": #9c27b0,
+ "lighten-5": #f3e5f5,
+ "lighten-4": #e1bee7,
+ "lighten-3": #ce93d8,
+ "lighten-2": #ba68c8,
+ "lighten-1": #ab47bc,
+ "darken-1": #8e24aa,
+ "darken-2": #7b1fa2,
+ "darken-3": #6a1b9a,
+ "darken-4": #4a148c,
+ "accent-1": #ea80fc,
+ "accent-2": #e040fb,
+ "accent-3": #d500f9,
+ "accent-4": #aa00ff
+);
+
+$deep-purple: (
+ "base": #673ab7,
+ "lighten-5": #ede7f6,
+ "lighten-4": #d1c4e9,
+ "lighten-3": #b39ddb,
+ "lighten-2": #9575cd,
+ "lighten-1": #7e57c2,
+ "darken-1": #5e35b1,
+ "darken-2": #512da8,
+ "darken-3": #4527a0,
+ "darken-4": #311b92,
+ "accent-1": #b388ff,
+ "accent-2": #7c4dff,
+ "accent-3": #651fff,
+ "accent-4": #6200ea
+);
+
+$indigo: (
+ "base": #3f51b5,
+ "lighten-5": #e8eaf6,
+ "lighten-4": #c5cae9,
+ "lighten-3": #9fa8da,
+ "lighten-2": #7986cb,
+ "lighten-1": #5c6bc0,
+ "darken-1": #3949ab,
+ "darken-2": #303f9f,
+ "darken-3": #283593,
+ "darken-4": #1a237e,
+ "accent-1": #8c9eff,
+ "accent-2": #536dfe,
+ "accent-3": #3d5afe,
+ "accent-4": #304ffe
+);
+
+$blue: (
+ "base": #2196F3,
+ "lighten-5": #E3F2FD,
+ "lighten-4": #BBDEFB,
+ "lighten-3": #90CAF9,
+ "lighten-2": #64B5F6,
+ "lighten-1": #42A5F5,
+ "darken-1": #1E88E5,
+ "darken-2": #1976D2,
+ "darken-3": #1565C0,
+ "darken-4": #0D47A1,
+ "accent-1": #82B1FF,
+ "accent-2": #448AFF,
+ "accent-3": #2979FF,
+ "accent-4": #2962FF
+);
+
+$light-blue: (
+ "base": #03a9f4,
+ "lighten-5": #e1f5fe,
+ "lighten-4": #b3e5fc,
+ "lighten-3": #81d4fa,
+ "lighten-2": #4fc3f7,
+ "lighten-1": #29b6f6,
+ "darken-1": #039be5,
+ "darken-2": #0288d1,
+ "darken-3": #0277bd,
+ "darken-4": #01579b,
+ "accent-1": #80d8ff,
+ "accent-2": #40c4ff,
+ "accent-3": #00b0ff,
+ "accent-4": #0091ea
+);
+
+$cyan: (
+ "base": #00bcd4,
+ "lighten-5": #e0f7fa,
+ "lighten-4": #b2ebf2,
+ "lighten-3": #80deea,
+ "lighten-2": #4dd0e1,
+ "lighten-1": #26c6da,
+ "darken-1": #00acc1,
+ "darken-2": #0097a7,
+ "darken-3": #00838f,
+ "darken-4": #006064,
+ "accent-1": #84ffff,
+ "accent-2": #18ffff,
+ "accent-3": #00e5ff,
+ "accent-4": #00b8d4
+);
+
+$teal: (
+ "base": #009688,
+ "lighten-5": #e0f2f1,
+ "lighten-4": #b2dfdb,
+ "lighten-3": #80cbc4,
+ "lighten-2": #4db6ac,
+ "lighten-1": #26a69a,
+ "darken-1": #00897b,
+ "darken-2": #00796b,
+ "darken-3": #00695c,
+ "darken-4": #004d40,
+ "accent-1": #a7ffeb,
+ "accent-2": #64ffda,
+ "accent-3": #1de9b6,
+ "accent-4": #00bfa5
+);
+
+$green: (
+ "base": #4CAF50,
+ "lighten-5": #E8F5E9,
+ "lighten-4": #C8E6C9,
+ "lighten-3": #A5D6A7,
+ "lighten-2": #81C784,
+ "lighten-1": #66BB6A,
+ "darken-1": #43A047,
+ "darken-2": #388E3C,
+ "darken-3": #2E7D32,
+ "darken-4": #1B5E20,
+ "accent-1": #B9F6CA,
+ "accent-2": #69F0AE,
+ "accent-3": #00E676,
+ "accent-4": #00C853
+);
+
+$light-green: (
+ "base": #8bc34a,
+ "lighten-5": #f1f8e9,
+ "lighten-4": #dcedc8,
+ "lighten-3": #c5e1a5,
+ "lighten-2": #aed581,
+ "lighten-1": #9ccc65,
+ "darken-1": #7cb342,
+ "darken-2": #689f38,
+ "darken-3": #558b2f,
+ "darken-4": #33691e,
+ "accent-1": #ccff90,
+ "accent-2": #b2ff59,
+ "accent-3": #76ff03,
+ "accent-4": #64dd17
+);
+
+$lime: (
+ "base": #cddc39,
+ "lighten-5": #f9fbe7,
+ "lighten-4": #f0f4c3,
+ "lighten-3": #e6ee9c,
+ "lighten-2": #dce775,
+ "lighten-1": #d4e157,
+ "darken-1": #c0ca33,
+ "darken-2": #afb42b,
+ "darken-3": #9e9d24,
+ "darken-4": #827717,
+ "accent-1": #f4ff81,
+ "accent-2": #eeff41,
+ "accent-3": #c6ff00,
+ "accent-4": #aeea00
+);
+
+$yellow: (
+ "base": #ffeb3b,
+ "lighten-5": #fffde7,
+ "lighten-4": #fff9c4,
+ "lighten-3": #fff59d,
+ "lighten-2": #fff176,
+ "lighten-1": #ffee58,
+ "darken-1": #fdd835,
+ "darken-2": #fbc02d,
+ "darken-3": #f9a825,
+ "darken-4": #f57f17,
+ "accent-1": #ffff8d,
+ "accent-2": #ffff00,
+ "accent-3": #ffea00,
+ "accent-4": #ffd600
+);
+
+$amber: (
+ "base": #ffc107,
+ "lighten-5": #fff8e1,
+ "lighten-4": #ffecb3,
+ "lighten-3": #ffe082,
+ "lighten-2": #ffd54f,
+ "lighten-1": #ffca28,
+ "darken-1": #ffb300,
+ "darken-2": #ffa000,
+ "darken-3": #ff8f00,
+ "darken-4": #ff6f00,
+ "accent-1": #ffe57f,
+ "accent-2": #ffd740,
+ "accent-3": #ffc400,
+ "accent-4": #ffab00
+);
+
+$orange: (
+ "base": #ff9800,
+ "lighten-5": #fff3e0,
+ "lighten-4": #ffe0b2,
+ "lighten-3": #ffcc80,
+ "lighten-2": #ffb74d,
+ "lighten-1": #ffa726,
+ "darken-1": #fb8c00,
+ "darken-2": #f57c00,
+ "darken-3": #ef6c00,
+ "darken-4": #e65100,
+ "accent-1": #ffd180,
+ "accent-2": #ffab40,
+ "accent-3": #ff9100,
+ "accent-4": #ff6d00
+);
+
+$deep-orange: (
+ "base": #ff5722,
+ "lighten-5": #fbe9e7,
+ "lighten-4": #ffccbc,
+ "lighten-3": #ffab91,
+ "lighten-2": #ff8a65,
+ "lighten-1": #ff7043,
+ "darken-1": #f4511e,
+ "darken-2": #e64a19,
+ "darken-3": #d84315,
+ "darken-4": #bf360c,
+ "accent-1": #ff9e80,
+ "accent-2": #ff6e40,
+ "accent-3": #ff3d00,
+ "accent-4": #dd2c00
+);
+
+$brown: (
+ "base": #795548,
+ "lighten-5": #efebe9,
+ "lighten-4": #d7ccc8,
+ "lighten-3": #bcaaa4,
+ "lighten-2": #a1887f,
+ "lighten-1": #8d6e63,
+ "darken-1": #6d4c41,
+ "darken-2": #5d4037,
+ "darken-3": #4e342e,
+ "darken-4": #3e2723
+);
+
+$blue-grey: (
+ "base": #607d8b,
+ "lighten-5": #eceff1,
+ "lighten-4": #cfd8dc,
+ "lighten-3": #b0bec5,
+ "lighten-2": #90a4ae,
+ "lighten-1": #78909c,
+ "darken-1": #546e7a,
+ "darken-2": #455a64,
+ "darken-3": #37474f,
+ "darken-4": #263238
+);
+
+$grey: (
+ "base": #9e9e9e,
+ "lighten-5": #fafafa,
+ "lighten-4": #f5f5f5,
+ "lighten-3": #eeeeee,
+ "lighten-2": #e0e0e0,
+ "lighten-1": #bdbdbd,
+ "darken-1": #757575,
+ "darken-2": #616161,
+ "darken-3": #424242,
+ "darken-4": #212121
+);
+
+$shades: (
+ "black": #000000,
+ "white": #FFFFFF,
+ "transparent": transparent
+);
+
+$colors: (
+ "materialize-red": $materialize-red,
+ "red": $red,
+ "pink": $pink,
+ "purple": $purple,
+ "deep-purple": $deep-purple,
+ "indigo": $indigo,
+ "blue": $blue,
+ "light-blue": $light-blue,
+ "cyan": $cyan,
+ "teal": $teal,
+ "green": $green,
+ "light-green": $light-green,
+ "lime": $lime,
+ "yellow": $yellow,
+ "amber": $amber,
+ "orange": $orange,
+ "deep-orange": $deep-orange,
+ "brown": $brown,
+ "blue-grey": $blue-grey,
+ "grey": $grey,
+ "shades": $shades
+);
+
+
+// Color Classes
+
+@each $color_name, $color in $colors {
+ @each $color_type, $color_value in $color {
+ @if $color_type == "base" {
+ .#{$color_name} {
+ background-color: $color_value !important;
+ }
+ .#{$color_name}-text {
+ color: $color_value !important;
+ }
+ }
+ @else {
+ .#{$color_name}.#{$color_type} {
+ background-color: $color_value !important;
+ }
+ .#{$color_name}-text.text-#{$color_type} {
+ color: $color_value !important;
+ }
+ }
+ }
+}
+
+// Shade classes
+@each $color, $color_value in $shades {
+ .#{$color} {
+ background-color: $color_value !important;
+ }
+ .#{$color}-text {
+ color: $color_value !important;
+ }
+}
+
+
+// usage: color("name_of_color", "type_of_color")
+// to avoid to repeating map-get($colors, ...)
+
+@function color($color, $type) {
+ @if map-has-key($colors, $color) {
+ $curr_color: map-get($colors, $color);
+ @if map-has-key($curr_color, $type) {
+ @return map-get($curr_color, $type);
+ }
+ }
+ @warn "Unknown `#{name}` in $colors.";
+ @return null;
+}
+
diff --git a/docs/assets/css/components/_dropdown.scss b/docs/assets/css/components/_dropdown.scss
new file mode 100644
index 0000000..71ab9f5
--- /dev/null
+++ b/docs/assets/css/components/_dropdown.scss
@@ -0,0 +1,57 @@
+.dropdown-content {
+ @extend .z-depth-1;
+ background-color: $dropdown-bg-color;
+ margin: 0;
+ display: none;
+ min-width: 100px;
+ max-height: 650px;
+ overflow-y: auto;
+ opacity: 0;
+ position: absolute;
+ z-index: 999;
+ will-change: width, height;
+
+ li {
+ clear: both;
+ color: $off-black;
+ cursor: pointer;
+ min-height: $dropdown-item-height;
+ line-height: 1.5rem;
+ width: 100%;
+ text-align: left;
+ text-transform: none;
+
+ &:hover, &.active, &.selected {
+ background-color: $dropdown-hover-bg-color;
+ }
+
+ &.active.selected {
+ background-color: darken($dropdown-hover-bg-color, 5%);
+ }
+
+ &.divider {
+ min-height: 0;
+ height: 1px;
+ }
+
+ & > a, & > span {
+ font-size: 16px;
+ color: $dropdown-color;
+ display: block;
+ line-height: 22px;
+ padding: (($dropdown-item-height - 22) / 2) 16px;
+ }
+
+ & > span > label {
+ top: 1px;
+ left: 3px;
+ height: 18px;
+ }
+
+ // Icon alignment override
+ & > a > i {
+ height: inherit;
+ line-height: inherit;
+ }
+ }
+}
diff --git a/docs/assets/css/components/_global.scss b/docs/assets/css/components/_global.scss
new file mode 100644
index 0000000..f4eea30
--- /dev/null
+++ b/docs/assets/css/components/_global.scss
@@ -0,0 +1,772 @@
+//Default styles
+
+html {
+ box-sizing: border-box;
+}
+*, *:before, *:after {
+ box-sizing: inherit;
+}
+
+body {
+ // display: flex;
+ // min-height: 100vh;
+ // flex-direction: column;
+}
+
+main {
+ // flex: 1 0 auto;
+}
+
+ul {
+ &.browser-default {
+ list-style-type: initial;
+ }
+
+ list-style-type: none;
+}
+
+a {
+ color: $link-color;
+ text-decoration: none;
+
+ // Gets rid of tap active state
+ -webkit-tap-highlight-color: transparent;
+}
+
+
+// Positioning
+.valign-wrapper {
+ display: flex;
+ align-items: center;
+
+ .valign {
+ display: block;
+ }
+}
+
+
+ul {
+ padding: 0;
+ li {
+ list-style-type: none;
+ }
+}
+
+// classic clearfix
+.clearfix {
+ clear: both;
+}
+
+
+// Z-levels
+.z-depth-0 {
+ box-shadow: none !important;
+}
+.z-depth-1{
+ box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
+}
+.z-depth-1-half{
+ box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
+}
+.z-depth-2{
+ box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+}
+.z-depth-3{
+ box-shadow: 0 12px 15px 0 rgba(0, 0, 0, 0.24), 0 17px 50px 0 rgba(0, 0, 0, 0.19);
+}
+.z-depth-4{
+ box-shadow: 0 16px 28px 0 rgba(0, 0, 0, 0.22), 0 25px 55px 0 rgba(0, 0, 0, 0.21);
+}
+.z-depth-5{
+ box-shadow: 0 27px 24px 0 rgba(0, 0, 0, 0.2), 0 40px 77px 0 rgba(0, 0, 0, 0.22);
+}
+
+.hoverable {
+ transition: box-shadow .25s;
+ box-shadow: 0;
+}
+
+.hoverable:hover {
+ transition: box-shadow .25s;
+ box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+}
+
+// Dividers
+
+.divider {
+ height: 1px;
+ overflow: hidden;
+ background-color: color("grey", "lighten-2");
+}
+
+
+// Blockquote
+
+blockquote {
+ margin: 20px 0;
+ padding-left: 1.5rem;
+ border-left: 5px solid $primary-color;
+}
+
+// Icon Styles
+
+i {
+ line-height: inherit;
+
+ &.left {
+ float: left;
+ margin-right: 15px;
+ }
+ &.right {
+ float: right;
+ margin-left: 15px;
+ }
+ &.tiny {
+ font-size: 1rem;
+ }
+ &.small {
+ font-size: 2rem;
+ }
+ &.medium {
+ font-size: 4rem;
+ }
+ &.large {
+ font-size: 6rem;
+ }
+}
+
+// Images
+img.responsive-img,
+video.responsive-video {
+ max-width: 100%;
+ height: auto;
+}
+
+
+// Pagination
+
+.pagination {
+
+ li {
+ display: inline-block;
+ font-size: 1.2rem;
+ padding: 0 10px;
+ line-height: 30px;
+ border-radius: 2px;
+ text-align: center;
+
+ a { color: #444; }
+
+ &.active a { color: #fff; }
+
+ &.active { background-color: $primary-color; }
+
+ &.disabled a {
+ cursor: default;
+ color: #999;
+ }
+
+ i {
+ font-size: 2.2rem;
+ vertical-align: middle;
+ }
+ }
+
+
+ li.pages ul li {
+ display: inline-block;
+ float: none;
+ }
+}
+@media #{$medium-and-down} {
+ .pagination {
+ width: 100%;
+
+ li.prev,
+ li.next {
+ width: 10%;
+ }
+
+ li.pages {
+ width: 80%;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+ }
+}
+
+// Breadcrumbs
+.breadcrumb {
+ font-size: 18px;
+ color: rgba(255,255,255, .7);
+
+ i,
+ [class^="mdi-"], [class*="mdi-"],
+ i.material-icons {
+ display: inline-block;
+ float: left;
+ font-size: 24px;
+ }
+
+ &:before {
+ content: '\E5CC';
+ color: rgba(255,255,255, .7);
+ vertical-align: top;
+ display: inline-block;
+ font-family: 'Material Icons';
+ font-weight: normal;
+ font-style: normal;
+ font-size: 25px;
+ margin: 0 10px 0 8px;
+ -webkit-font-smoothing: antialiased;
+ }
+
+ &:first-child:before {
+ display: none;
+ }
+
+ &:last-child {
+ color: #fff;
+ }
+}
+
+
+// Parallax
+.parallax-container {
+ position: relative;
+ overflow: hidden;
+ height: 500px;
+}
+
+.parallax {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: -1;
+
+ img {
+ display: none;
+ position: absolute;
+ left: 50%;
+ bottom: 0;
+ min-width: 100%;
+ min-height: 100%;
+ -webkit-transform: translate3d(0,0,0);
+ transform: translate3d(0,0,0);
+ transform: translateX(-50%);
+ }
+}
+
+// Pushpin
+.pin-top, .pin-bottom {
+ position: relative;
+}
+.pinned {
+ position: fixed !important;
+}
+
+/*********************
+ Transition Classes
+**********************/
+
+ul.staggered-list li {
+ opacity: 0;
+}
+
+.fade-in {
+ opacity: 0;
+ transform-origin: 0 50%;
+}
+
+
+/*********************
+ Media Query Classes
+**********************/
+.hide-on-small-only, .hide-on-small-and-down {
+ @media #{$small-and-down} {
+ display: none !important;
+ }
+}
+.hide-on-med-and-down {
+ @media #{$medium-and-down} {
+ display: none !important;
+ }
+}
+.hide-on-med-and-up {
+ @media #{$medium-and-up} {
+ display: none !important;
+ }
+}
+.hide-on-med-only {
+ @media only screen and (min-width: $small-screen) and (max-width: $medium-screen) {
+ display: none !important;
+ }
+}
+.hide-on-large-only {
+ @media #{$large-and-up} {
+ display: none !important;
+ }
+}
+.show-on-large {
+ @media #{$large-and-up} {
+ display: block !important;
+ }
+}
+.show-on-medium {
+ @media only screen and (min-width: $small-screen) and (max-width: $medium-screen) {
+ display: block !important;
+ }
+}
+.show-on-small {
+ @media #{$small-and-down} {
+ display: block !important;
+ }
+}
+.show-on-medium-and-up {
+ @media #{$medium-and-up} {
+ display: block !important;
+ }
+}
+.show-on-medium-and-down {
+ @media #{$medium-and-down} {
+ display: block !important;
+ }
+}
+
+
+// Center text on mobile
+.center-on-small-only {
+ @media #{$small-and-down} {
+ text-align: center;
+ }
+}
+
+// Footer
+footer.page-footer {
+ margin-top: 20px;
+ padding-top: 20px;
+ background-color: $footer-bg-color;
+
+ .footer-copyright {
+ overflow: hidden;
+ height: 50px;
+ line-height: 50px;
+ color: rgba(255,255,255,.8);
+ background-color: rgba(51,51,51,.08);
+ @extend .light;
+ }
+}
+
+// Tables
+table, th, td {
+ border: none;
+}
+
+table {
+ width:100%;
+ display: table;
+
+ &.bordered > thead > tr,
+ &.bordered > tbody > tr {
+ border-bottom: 1px solid $table-border-color;
+ }
+
+ &.striped > tbody {
+ > tr:nth-child(odd) {
+ background-color: $table-striped-color;
+ }
+
+ > tr > td {
+ border-radius: 0;
+ }
+ }
+
+ &.highlight > tbody > tr {
+ transition: background-color .25s ease;
+ &:hover {
+ background-color: $table-striped-color;
+ }
+ }
+
+ &.centered {
+ thead tr th, tbody tr td {
+ text-align: center;
+ }
+ }
+
+}
+
+thead {
+ border-bottom: 1px solid $table-border-color;
+}
+
+td, th{
+ padding: 15px 5px;
+ display: table-cell;
+ text-align: left;
+ vertical-align: middle;
+ border-radius: 2px;
+}
+
+// Responsive Table
+@media #{$medium-and-down} {
+
+ table.responsive-table {
+ width: 100%;
+ border-collapse: collapse;
+ border-spacing: 0;
+ display: block;
+ position: relative;
+
+ td:empty:before {
+ content: '\00a0';
+ }
+
+ th,
+ td {
+ margin: 0;
+ vertical-align: top;
+ }
+
+ th { text-align: left; }
+ thead {
+ display: block;
+ float: left;
+
+ tr {
+ display: block;
+ padding: 0 10px 0 0;
+
+ th::before {
+ content: "\00a0";
+ }
+ }
+ }
+ tbody {
+ display: block;
+ width: auto;
+ position: relative;
+ overflow-x: auto;
+ white-space: nowrap;
+
+ tr {
+ display: inline-block;
+ vertical-align: top;
+ }
+ }
+ th {
+ display: block;
+ text-align: right;
+ }
+ td {
+ display: block;
+ min-height: 1.25em;
+ text-align: left;
+ }
+ tr { padding: 0 10px; }
+
+ /* sort out borders */
+ thead {
+ border: 0;
+ border-right: 1px solid $table-border-color;
+ }
+
+ &.bordered {
+ th { border-bottom: 0; border-left: 0; }
+ td { border-left: 0; border-right: 0; border-bottom: 0; }
+ tr { border: 0; }
+ tbody tr { border-right: 1px solid $table-border-color; }
+ }
+
+ }
+
+}
+
+
+// Collections
+.collection {
+ margin: $element-top-margin 0 $element-bottom-margin 0;
+ border: 1px solid $collection-border-color;
+ border-radius: 2px;
+ overflow: hidden;
+ position: relative;
+
+ .collection-item {
+ background-color: $collection-bg-color;
+ line-height: 1.5rem;
+ padding: 10px 20px;
+ margin: 0;
+ border-bottom: 1px solid $collection-border-color;
+
+ // Avatar Collection
+ &.avatar {
+ min-height: 84px;
+ padding-left: 72px;
+ position: relative;
+
+ .circle {
+ position: absolute;
+ width: 42px;
+ height: 42px;
+ overflow: hidden;
+ left: 15px;
+ display: inline-block;
+ vertical-align: middle;
+ }
+ i.circle {
+ font-size: 18px;
+ line-height: 42px;
+ color: #fff;
+ background-color: #999;
+ text-align: center;
+ }
+
+
+ .title {
+ font-size: 16px;
+ }
+
+ p {
+ margin: 0;
+ }
+
+ .secondary-content {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ }
+
+ }
+
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &.active {
+ background-color: $collection-active-bg-color;
+ color: $collection-active-color;
+
+ .secondary-content {
+ color: #fff;
+ }
+ }
+ }
+ a.collection-item{
+ display: block;
+ transition: .25s;
+ color: $collection-link-color;
+ &:not(.active) {
+ &:hover {
+ background-color: $collection-hover-bg-color;
+ }
+ }
+ }
+
+ &.with-header {
+ .collection-header {
+ background-color: $collection-bg-color;
+ border-bottom: 1px solid $collection-border-color;
+ padding: 10px 20px;
+ }
+ .collection-item {
+ padding-left: 30px;
+ }
+ .collection-item.avatar {
+ padding-left: 72px;
+ }
+ }
+
+}
+// Made less specific to allow easier overriding
+.secondary-content {
+ float: right;
+ color: $secondary-color;
+}
+.collapsible .collection {
+ margin: 0;
+ border: none;
+}
+
+
+
+// Badges
+span.badge {
+ min-width: 3rem;
+ padding: 0 6px;
+ text-align: center;
+ font-size: 1rem;
+ line-height: inherit;
+ color: color('grey', 'darken-1');
+ position: absolute;
+ right: 15px;
+ box-sizing: border-box;
+
+ &.new {
+ font-weight: 300;
+ font-size: 0.8rem;
+ color: #fff;
+ background-color: $badge-bg-color;
+ border-radius: 2px;
+ }
+ &.new:after {
+ content: " new";
+ }
+}
+nav ul a span.badge {
+ position: static;
+ margin-left: 4px;
+ line-height: 0;
+}
+
+// Responsive Videos
+.video-container {
+ position: relative;
+ padding-bottom: 56.25%;
+ height: 0;
+ overflow: hidden;
+
+ iframe, object, embed {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+}
+
+// Progress Bar
+.progress {
+ position: relative;
+ height: 4px;
+ display: block;
+ width: 100%;
+ background-color: lighten($progress-bar-color, 40%);
+ border-radius: 2px;
+ margin: $element-top-margin 0 $element-bottom-margin 0;
+ overflow: hidden;
+ .determinate {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ background-color: $progress-bar-color;
+ transition: width .3s linear;
+ }
+ .indeterminate {
+ background-color: $progress-bar-color;
+ &:before {
+ content: '';
+ position: absolute;
+ background-color: inherit;
+ top: 0;
+ left:0;
+ bottom: 0;
+ will-change: left, right;
+ // Custom bezier
+ animation: indeterminate 2.1s cubic-bezier(0.650, 0.815, 0.735, 0.395) infinite;
+
+ }
+ &:after {
+ content: '';
+ position: absolute;
+ background-color: inherit;
+ top: 0;
+ left:0;
+ bottom: 0;
+ will-change: left, right;
+ // Custom bezier
+ animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.840, 0.440, 1.000) infinite;
+ animation-delay: 1.15s;
+ }
+ }
+}
+@keyframes indeterminate {
+ 0% {
+ left: -35%;
+ right:100%;
+ }
+ 60% {
+ left: 100%;
+ right: -90%;
+ }
+ 100% {
+ left: 100%;
+ right: -90%;
+ }
+}
+
+@keyframes indeterminate-short {
+ 0% {
+ left: -200%;
+ right: 100%;
+ }
+ 60% {
+ left: 107%;
+ right: -8%;
+ }
+ 100% {
+ left: 107%;
+ right: -8%;
+ }
+}
+
+
+/*******************
+ Utility Classes
+*******************/
+
+.hide {
+ display: none !important;
+}
+
+// Text Align
+.left-align {
+ text-align: left;
+}
+.right-align {
+ text-align: right
+}
+.center, .center-align {
+ text-align: center;
+}
+
+.left {
+ float: left !important;
+}
+.right {
+ float: right !important;
+}
+
+// No Text Select
+.no-select {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.circle {
+ border-radius: 50%;
+}
+
+.center-block {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.truncate {
+ display: block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.no-padding {
+ padding: 0 !important;
+}
diff --git a/docs/assets/css/components/_grid.scss b/docs/assets/css/components/_grid.scss
new file mode 100644
index 0000000..8985cb0
--- /dev/null
+++ b/docs/assets/css/components/_grid.scss
@@ -0,0 +1,146 @@
+.container {
+ margin: 0 auto;
+ max-width: 1280px;
+ width: 90%;
+}
+@media #{$medium-and-up} {
+ .container {
+ width: 85%;
+ }
+}
+@media #{$large-and-up} {
+ .container {
+ width: 70%;
+ }
+}
+.container .row {
+ margin-left: (-1 * $gutter-width / 2);
+ margin-right: (-1 * $gutter-width / 2);
+}
+
+.section {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+
+ &.no-pad {
+ padding: 0;
+ }
+ &.no-pad-bot {
+ padding-bottom: 0;
+ }
+ &.no-pad-top {
+ padding-top: 0;
+ }
+}
+
+
+.row {
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 20px;
+
+ // Clear floating children
+ &:after {
+ content: "";
+ display: table;
+ clear: both;
+ }
+
+ .col {
+ float: left;
+ box-sizing: border-box;
+ padding: 0 $gutter-width / 2;
+
+ &[class*="push-"],
+ &[class*="pull-"] {
+ position: relative;
+ }
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ &.s#{$i} {
+ width: $perc;
+ margin-left: auto;
+ left: auto;
+ right: auto;
+ }
+ $i: $i + 1;
+ }
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ &.offset-s#{$i} {
+ margin-left: $perc;
+ }
+ &.pull-s#{$i} {
+ right: $perc;
+ }
+ &.push-s#{$i} {
+ left: $perc;
+ }
+ $i: $i + 1;
+ }
+
+ @media #{$medium-and-up} {
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ &.m#{$i} {
+ width: $perc;
+ margin-left: auto;
+ left: auto;
+ right: auto;
+ }
+ $i: $i + 1
+ }
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ &.offset-m#{$i} {
+ margin-left: $perc;
+ }
+ &.pull-m#{$i} {
+ right: $perc;
+ }
+ &.push-m#{$i} {
+ left: $perc;
+ }
+ $i: $i + 1;
+ }
+ }
+
+ @media #{$large-and-up} {
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ &.l#{$i} {
+ width: $perc;
+ margin-left: auto;
+ left: auto;
+ right: auto;
+ }
+ $i: $i + 1;
+ }
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ &.offset-l#{$i} {
+ margin-left: $perc;
+ }
+ &.pull-l#{$i} {
+ right: $perc;
+ }
+ &.push-l#{$i} {
+ left: $perc;
+ }
+ $i: $i + 1;
+ }
+ }
+ }
+}
diff --git a/docs/assets/css/components/_icons-material-design.scss b/docs/assets/css/components/_icons-material-design.scss
new file mode 100644
index 0000000..d8d91c1
--- /dev/null
+++ b/docs/assets/css/components/_icons-material-design.scss
@@ -0,0 +1,5 @@
+/* This is needed for some mobile phones to display the Google Icon font properly */
+.material-icons {
+ text-rendering: optimizeLegibility;
+ font-feature-settings: 'liga';
+}
diff --git a/docs/assets/css/components/_materialbox.scss b/docs/assets/css/components/_materialbox.scss
new file mode 100644
index 0000000..ee37426
--- /dev/null
+++ b/docs/assets/css/components/_materialbox.scss
@@ -0,0 +1,42 @@
+.materialboxed {
+ display: block;
+ cursor: zoom-in;
+ position: relative;
+ transition: opacity .4s;
+
+ &:hover {
+ &:not(.active) {
+ opacity: .8;
+ }
+ will-change: left, top, width, height;
+ }
+}
+
+.materialboxed.active {
+ cursor: zoom-out;
+}
+
+#materialbox-overlay {
+ position:fixed;
+ top:0;
+ left:0;
+ right: 0;
+ bottom: 0;
+ background-color: #292929;
+ z-index: 1000;
+
+ will-change: opacity;
+}
+.materialbox-caption {
+ position: fixed;
+ display: none;
+ color: #fff;
+ line-height: 50px;
+ bottom: 0;
+ width: 100%;
+ text-align: center;
+ padding: 0% 15%;
+ height: 50px;
+ z-index: 1000;
+ -webkit-font-smoothing: antialiased;
+}
\ No newline at end of file
diff --git a/docs/assets/css/components/_me.scss b/docs/assets/css/components/_me.scss
new file mode 100644
index 0000000..3993bee
--- /dev/null
+++ b/docs/assets/css/components/_me.scss
@@ -0,0 +1,64 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+nav {
+ -webkit-box-shadow: none;
+ box-shadow: none;
+ background: none;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 55px;
+}
+
+section {
+ display: table;
+ width: 100%;
+}
+
+.fixed {
+ background-attachment: fixed;
+ background-size: cover;
+ background-repeat: no-repeat;
+}
+.v100 {
+ height: 100vh;
+}
+.v80 {
+ height: 80vh;
+}
+
+.v70 {
+ height: 70vh;
+}
+
+.v50 {
+ height: 50vh;
+}
+
+.radius-border{
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+.opacity-background{
+ background: rgba(0,0,0,.5);
+}
+
+.love-footer {
+ color:red;
+}
+
+.container-wide {
+ padding: 5em 2em;
+}
+
+.post-title {
+ font-size: 2em;
+}
+
+.post-date {
+ font-size: 0.8em;
+}
\ No newline at end of file
diff --git a/docs/assets/css/components/_mixins.scss b/docs/assets/css/components/_mixins.scss
new file mode 100644
index 0000000..054f8f6
--- /dev/null
+++ b/docs/assets/css/components/_mixins.scss
@@ -0,0 +1,5 @@
+// @mixin box-shadow-2($args1, $args2) {
+// -webkit-box-shadow: $args1, $args2;
+// -moz-box-shadow: $args1, $args2;
+// box-shadow: $args1, $args2;
+// }
\ No newline at end of file
diff --git a/docs/assets/css/components/_modal.scss b/docs/assets/css/components/_modal.scss
new file mode 100644
index 0000000..0a6dcfb
--- /dev/null
+++ b/docs/assets/css/components/_modal.scss
@@ -0,0 +1,90 @@
+.modal {
+ @extend .z-depth-4;
+
+ display: none;
+ position: fixed;
+ left: 0;
+ right: 0;
+ background-color: #fafafa;
+ padding: 0;
+ max-height: 70%;
+ width: 55%;
+ margin: auto;
+ overflow-y: auto;
+
+ border-radius: 2px;
+ will-change: top, opacity;
+
+ @media #{$medium-and-down} {
+ width: 80%;
+ }
+
+ h1,h2,h3,h4 {
+ margin-top: 0;
+ }
+
+ .modal-content {
+ padding: 24px;
+ }
+ .modal-close {
+ cursor: pointer;
+ }
+
+ .modal-footer {
+ border-radius: 0 0 2px 2px;
+ background-color: #fafafa;
+ padding: 4px 6px;
+ height: 56px;
+ width: 100%;
+
+ .btn, .btn-flat {
+ float: right;
+ margin: 6px 0;
+ }
+ }
+}
+.lean-overlay {
+ position: fixed;
+ z-index:999;
+ top: -100px;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ height: 125%;
+ width: 100%;
+ background: #000;
+ display: none;
+
+ will-change: opacity;
+}
+
+// Modal with fixed action footer
+.modal.modal-fixed-footer {
+ padding: 0;
+ height: 70%;
+
+ .modal-content {
+ position: absolute;
+ height: calc(100% - 56px);
+ max-height: 100%;
+ width: 100%;
+ overflow-y: auto;
+ }
+
+ .modal-footer {
+ border-top: 1px solid rgba(0,0,0,.1);
+ position: absolute;
+ bottom: 0;
+ }
+}
+
+// Modal Bottom Sheet Style
+.modal.bottom-sheet {
+ top: auto;
+ bottom: -100%;
+ margin: 0;
+ width: 100%;
+ max-height: 45%;
+ border-radius: 0;
+ will-change: bottom, opacity;
+}
diff --git a/docs/assets/css/components/_navbar.scss b/docs/assets/css/components/_navbar.scss
new file mode 100644
index 0000000..14fe92e
--- /dev/null
+++ b/docs/assets/css/components/_navbar.scss
@@ -0,0 +1,171 @@
+nav {
+ color: $navbar-font-color;
+ @extend .z-depth-1;
+ background-color: $primary-color;
+ width: 100%;
+ height: $navbar-height-mobile;
+ line-height: $navbar-height-mobile;
+
+ a { color: $navbar-font-color; }
+
+ i,
+ [class^="mdi-"], [class*="mdi-"],
+ i.material-icons {
+ display: block;
+ font-size: 2rem;
+ height: $navbar-height-mobile;
+ line-height: $navbar-height-mobile;
+ }
+
+ .nav-wrapper {
+ position: relative;
+ height: 100%;
+ }
+
+ @media #{$large-and-up} {
+ a.button-collapse { display: none; }
+ }
+
+
+ // Collapse button
+ .button-collapse {
+ float: left;
+ position: relative;
+ z-index: 1;
+ height: $navbar-height-mobile;
+
+ i {
+ font-size: 2.7rem;
+ height: $navbar-height-mobile;
+ line-height: $navbar-height-mobile;
+ }
+ }
+
+
+ // Logo
+ .brand-logo {
+ position: absolute;
+ color: $navbar-font-color;
+ display: inline-block;
+ font-size: $navbar-brand-font-size;
+ padding: 0;
+ white-space: nowrap;
+
+ &.center {
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ @media #{$medium-and-down} {
+ left: 50%;
+ transform: translateX(-50%);
+
+ &.left, &.right {
+ padding: 0;
+ transform: none;
+ }
+
+ &.left { left: 0.5rem; }
+ &.right {
+ right: 0.5rem;
+ left: auto;
+ }
+ }
+
+ &.right {
+ right: 0.5rem;
+ padding: 0;
+ }
+ }
+
+
+ // Navbar Links
+ ul {
+ margin: 0;
+
+ li {
+ transition: background-color .3s;
+ float: left;
+ padding: 0;
+
+ &.active {
+ background-color: rgba(0,0,0,.1);
+ }
+ }
+ a {
+ transition: background-color .3s;
+ font-size: 1rem;
+ color: $navbar-font-color;
+ display: inline-block;
+ padding: 0 15px;
+ cursor: pointer;
+
+ &.btn, &.btn-large, &.btn-flat, &.btn-floating {
+ margin-top: -2px;
+ margin-left: 15px;
+ margin-right: 15px;
+ }
+
+ &:hover {
+ background-color: rgba(0,0,0,.1);
+ }
+ }
+
+ &.left {
+ float: left;
+ }
+ }
+
+ // Navbar Search Form
+ .input-field {
+ margin: 0;
+
+ input {
+ height: 100%;
+ font-size: 1.2rem;
+ border: none;
+ padding-left: 2rem;
+
+ &:focus, &[type=text]:valid, &[type=password]:valid,
+ &[type=email]:valid, &[type=url]:valid, &[type=date]:valid {
+ border: none;
+ box-shadow: none;
+ }
+ }
+ label {
+ top: 0;
+ left: 0;
+
+ i {
+ color: rgba(255,255,255,.7);
+ transition: color .3s;
+ }
+ &.active i { color: $navbar-font-color; }
+ &.active {
+ transform: translateY(0);
+ }
+ }
+
+ }
+
+}
+
+// Fixed Navbar
+.navbar-fixed {
+ position: relative;
+ height: $navbar-height-mobile;
+ z-index: 998;
+
+ nav {
+ position: fixed;
+ }
+}
+@media #{$medium-and-up} {
+ nav, nav .nav-wrapper i, nav a.button-collapse, nav a.button-collapse i {
+ height: $navbar-height;
+ line-height: $navbar-height;
+ }
+ .navbar-fixed {
+ height: $navbar-height;
+ }
+}
diff --git a/docs/assets/css/components/_normalize.scss b/docs/assets/css/components/_normalize.scss
new file mode 100644
index 0000000..5e5e3c8
--- /dev/null
+++ b/docs/assets/css/components/_normalize.scss
@@ -0,0 +1,424 @@
+/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS and IE text size adjust after device orientation change,
+ * without disabling user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio,
+canvas,
+progress,
+video {
+ display: inline-block; /* 1 */
+ vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * Improve readability of focused elements when they are also in an
+ * active/hover state.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ color: inherit; /* 1 */
+ font: inherit; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+input {
+ line-height: normal;
+}
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ box-sizing: content-box; /* 2 */
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td,
+th {
+ padding: 0;
+}
diff --git a/docs/assets/css/components/_prefixer.scss b/docs/assets/css/components/_prefixer.scss
new file mode 100644
index 0000000..13376de
--- /dev/null
+++ b/docs/assets/css/components/_prefixer.scss
@@ -0,0 +1,384 @@
+//---------------------------------------------------
+// Sass Prefixer
+// -------------------------------------------------
+// TABLE OF CONTENTS
+// (*) denotes a syntax-sugar helper
+// -------------------------------------------------
+//
+// animation($args)
+// animation-delay($delay)
+// animation-direction($direction)
+// animation-duration($duration)
+// animation-fill-mode($mode)
+// animation-iteration-count($count)
+// animation-name($name)
+// animation-play-state($state)
+// animation-timing-function($function)
+// background-size($args)
+// inner-shadow($args) *
+// box-sizing($args)
+// border-box() *
+// content-box() *
+// columns($args)
+// column-count($count)
+// column-gap($gap)
+// column-rule($args)
+// column-width($width)
+// flexbox()
+// flex($args)
+// order($args)
+// align($args)
+// justify-content($args)
+// gradient($default,$start,$stop) *
+// linear-gradient-top($default,$color1,$stop1,$color2,$stop2,[$color3,$stop3,$color4,$stop4])*
+// linear-gradient-left($default,$color1,$stop1,$color2,$stop2,[$color3,$stop3,$color4,$stop4])*
+// perspective($pixels)
+// transform($args)
+// transform-origin($args)
+// transform-style($style)
+// rotate($deg)
+// scale($factor)
+// translate($x,$y)
+// translate3d($x,$y,$z)
+// translateHardware($x,$y) *
+// text-shadow($args)
+// transition($args)
+// transition-delay($delay)
+// transition-duration($duration)
+// transition-property($property)
+// transition-timing-function($function)
+
+
+// Animation
+
+// @mixin animation($args) {
+// -webkit-animation: $args;
+// -moz-animation: $args;
+// -ms-animation: $args;
+// -o-animation: $args;
+// animation: $args;
+// }
+// @mixin animation-delay($delay) {
+// -webkit-animation-delay: $delay;
+// -moz-animation-delay: $delay;
+// -ms-animation-delay: $delay;
+// -o-animation-delay: $delay;
+// animation-delay: $delay;
+// }
+// @mixin animation-direction($direction) {
+// -webkit-animation-direction: $direction;
+// -moz-animation-direction: $direction;
+// -ms-animation-direction: $direction;
+// -o-animation-direction: $direction;
+// }
+// @mixin animation-duration($duration) {
+// -webkit-animation-duration: $duration;
+// -moz-animation-duration: $duration;
+// -ms-animation-duration: $duration;
+// -o-animation-duration: $duration;
+// }
+// @mixin animation-fill-mode($mode) {
+// -webkit-animation-fill-mode: $mode;
+// -moz-animation-fill-mode: $mode;
+// -ms-animation-fill-mode: $mode;
+// -o-animation-fill-mode: $mode;
+// animation-fill-mode: $mode;
+// }
+// @mixin animation-iteration-count($count) {
+// -webkit-animation-iteration-count: $count;
+// -moz-animation-iteration-count: $count;
+// -ms-animation-iteration-count: $count;
+// -o-animation-iteration-count: $count;
+// animation-iteration-count: $count;
+// }
+// @mixin animation-name($name) {
+// -webkit-animation-name: $name;
+// -moz-animation-name: $name;
+// -ms-animation-name: $name;
+// -o-animation-name: $name;
+// animation-name: $name;
+// }
+// @mixin animation-play-state($state) {
+// -webkit-animation-play-state: $state;
+// -moz-animation-play-state: $state;
+// -ms-animation-play-state: $state;
+// -o-animation-play-state: $state;
+// animation-play-state: $state;
+// }
+// @mixin animation-timing-function($function) {
+// -webkit-animation-timing-function: $function;
+// -moz-animation-timing-function: $function;
+// -ms-animation-timing-function: $function;
+// -o-animation-timing-function: $function;
+// animation-timing-function: $function;
+// }
+
+// Keyframes
+// @mixin keyframes($animation-name) {
+// @-webkit-keyframes #{$animation-name} {
+// @content;
+// }
+// @-moz-keyframes #{$animation-name} {
+// @content;
+// }
+// @keyframes #{$animation-name} {
+// @content;
+// }
+// }
+
+// Backface-visibility
+
+// @mixin backface-visibility($args) {
+// -webkit-backface-visibility: $args;
+// -moz-backface-visibility: $args;
+// -ms-backface-visibility: $args;
+// backface-visibility: $args;
+// }
+
+
+// Background Size
+
+// @mixin background-size($args) {
+// -webkit-background-size: $args;
+// background-size: $args;
+// }
+
+// Box Sizing
+
+// @mixin box-sizing($args) {
+// -webkit-box-sizing: $args;
+// -moz-box-sizing: $args;
+// box-sizing: $args;
+// }
+// @mixin border-box(){
+// @include box-sizing(border-box);
+// }
+// @mixin content-box(){
+// @include box-sizing(content-box);
+// }
+
+
+// Columns
+
+// @mixin columns($args) {
+// -webkit-columns: $args;
+// -moz-columns: $args;
+// columns: $args;
+// }
+// @mixin column-count($count) {
+// -webkit-column-count: $count;
+// -moz-column-count: $count;
+// column-count: $count;
+// }
+// @mixin column-gap($gap) {
+// -webkit-column-gap: $gap;
+// -moz-column-gap: $gap;
+// column-gap: $gap;
+// }
+// @mixin column-width($width) {
+// -webkit-column-width: $width;
+// -moz-column-width: $width;
+// column-width: $width;
+// }
+// @mixin column-rule($args) {
+// -webkit-column-rule: $args;
+// -moz-column-rule: $args;
+// column-rule: $args;
+// }
+
+// Filter
+// @mixin filter($args) {
+// -webkit-filter: $args;
+// -moz-filter: $args;
+// -o-filter: $args;
+// -ms-filter: $args;
+// }
+
+// Flexbox
+// @mixin flexbox() {
+// display: -webkit-box;
+// display: -moz-box;
+// display: -ms-flexbox;
+// display: -webkit-flex;
+// display: flex;
+// }
+ // @mixin flex($values) {
+ // -webkit-box-flex: $values;
+ // -moz-box-flex: $values;
+ // -webkit-flex: $values;
+ // -ms-flex: $values;
+ // flex: $values;
+ // }
+ // @mixin order($val) {
+ // -webkit-box-ordinal-group: $val;
+ // -moz-box-ordinal-group: $val;
+ // -ms-flex-order: $val;
+ // -webkit-order: $val;
+ // order: $val;
+ // }
+ // @mixin align($align) {
+ // -webkit-flex-align: $align;
+ // -ms-flex-align: $align;
+ // -webkit-align-items: $align;
+ // align-items: $align;
+ // }
+ // @mixin justify-content($val) {
+ // -webkit-justify-content: $val;
+ // justify-content: $val;
+ // }
+// Gradients
+
+// @mixin gradient($default: #F5F5F5, $start: #EEE, $stop: #FFF) {
+// @include linear-gradient-top($default,$start,0%,$stop,100%);
+// }
+// @mixin linear-gradient-top($default,$color1,$stop1,$color2,$stop2) {
+// background-color: $default;
+// background-image: -webkit-gradient(linear, left top, left bottom, color-stop($stop1, $color1), color-stop($stop2 $color2));
+// background-image: -webkit-linear-gradient(top, $color1 $stop1, $color2 $stop2);
+// background-image: -moz-linear-gradient(top, $color1 $stop1, $color2 $stop2);
+// background-image: -ms-linear-gradient(top, $color1 $stop1, $color2 $stop2);
+// background-image: -o-linear-gradient(top, $color1 $stop1, $color2 $stop2);
+// background-image: linear-gradient(top, $color1 $stop1, $color2 $stop2);
+// }
+// @mixin linear-gradient-top2($default,$color1,$stop1,$color2,$stop2,$color3,$stop3) {
+// background-color: $default;
+// background-image: -webkit-gradient(linear, left top, left bottom, color-stop($stop1, $color1), color-stop($stop2 $color2), color-stop($stop3 $color3));
+// background-image: -webkit-linear-gradient(top, $color1 $stop1, $color2 $stop2, $color3 $stop3);
+// background-image: -moz-linear-gradient(top, $color1 $stop1, $color2 $stop2, $color3 $stop3);
+// background-image: -ms-linear-gradient(top, $color1 $stop1, $color2 $stop2, $color3 $stop3);
+// background-image: -o-linear-gradient(top, $color1 $stop1, $color2 $stop2, $color3 $stop3);
+// background-image: linear-gradient(top, $color1 $stop1, $color2 $stop2, $color3 $stop3);
+// }
+// @mixin linear-gradient-top3($default,$color1,$stop1,$color2,$stop2,$color3,$stop3,$color4,$stop4) {
+// background-color: $default;
+// background-image: -webkit-gradient(linear, left top, left bottom, color-stop($stop1, $color1), color-stop($stop2 $color2), color-stop($stop3 $color3), color-stop($stop4 $color4));
+// background-image: -webkit-linear-gradient(top, $color1 $stop1, $color2 $stop2, $color3 $stop3, $color4 $stop4);
+// background-image: -moz-linear-gradient(top, $color1 $stop1, $color2 $stop2, $color3 $stop3, $color4 $stop4);
+// background-image: -ms-linear-gradient(top, $color1 $stop1, $color2 $stop2, $color3 $stop3, $color4 $stop4);
+// background-image: -o-linear-gradient(top, $color1 $stop1, $color2 $stop2, $color3 $stop3, $color4 $stop4);
+// background-image: linear-gradient(top, $color1 $stop1, $color2 $stop2, $color3 $stop3, $color4 $stop4);
+// }
+// @mixin linear-gradient-left($default,$color1,$stop1,$color2,$stop2) {
+// background-color: $default;
+// background-image: -webkit-gradient(linear, left top, left top, color-stop($stop1, $color1), color-stop($stop2 $color2));
+// background-image: -webkit-linear-gradient(left, $color1 $stop1, $color2 $stop2);
+// background-image: -moz-linear-gradient(left, $color1 $stop1, $color2 $stop2);
+// background-image: -ms-linear-gradient(left, $color1 $stop1, $color2 $stop2);
+// background-image: -o-linear-gradient(left, $color1 $stop1, $color2 $stop2);
+// background-image: linear-gradient(left, $color1 $stop1, $color2 $stop2);
+// }
+// @mixin linear-gradient-left2($default,$color1,$stop1,$color2,$stop2,$color3,$stop3) {
+// background-color: $default;
+// background-image: -webkit-gradient(linear, left top, left top, color-stop($stop1, $color1), color-stop($stop2 $color2), color-stop($stop3 $color3));
+// background-image: -webkit-linear-gradient(left, $color1 $stop1, $color2 $stop2, $color3 $stop3);
+// background-image: -moz-linear-gradient(left, $color1 $stop1, $color2 $stop2, $color3 $stop3);
+// background-image: -ms-linear-gradient(left, $color1 $stop1, $color2 $stop2, $color3 $stop3);
+// background-image: -o-linear-gradient(left, $color1 $stop1, $color2 $stop2, $color3 $stop3);
+// background-image: linear-gradient(left, $color1 $stop1, $color2 $stop2, $color3 $stop3);
+// }
+// @mixin linear-gradient-left3($default,$color1,$stop1,$color2,$stop2,$color3,$stop3,$color4,$stop4) {
+// background-color: $default;
+// background-image: -webkit-gradient(linear, left top, left top, color-stop($stop1, $color1), color-stop($stop2 $color2), color-stop($stop3 $color3), color-stop($stop4 $color4));
+// background-image: -webkit-linear-gradient(left, $color1 $stop1, $color2 $stop2, $color3 $stop3, $color4 $stop4);
+// background-image: -moz-linear-gradient(left, $color1 $stop1, $color2 $stop2, $color3 $stop3, $color4 $stop4);
+// background-image: -ms-linear-gradient(left, $color1 $stop1, $color2 $stop2, $color3 $stop3, $color4 $stop4);
+// background-image: -o-linear-gradient(left, $color1 $stop1, $color2 $stop2, $color3 $stop3, $color4 $stop4);
+// background-image: linear-gradient(left, $color1 $stop1, $color2 $stop2, $color3 $stop3, $color4 $stop4);
+// }
+
+// Perspective
+@mixin perspective($pixels) {
+ perspective: $pixels;
+ -webkit-perspective: $pixels;
+}
+
+
+// Text Shadow
+
+// @mixin text-shadow($args) {
+// text-shadow: $args;
+// }
+
+
+// Transforms
+
+// @mixin transform($args) {
+// -webkit-transform: $args;
+// -moz-transform: $args;
+// -ms-transform: $args;
+// -o-transform: $args;
+// transform: $args;
+// }
+// @mixin transform-origin($args) {
+// -webkit-transform-origin: $args;
+// -moz-transform-origin: $args;
+// -ms-transform-origin: $args;
+// -o-transform-origin: $args;
+// transform-origin: $args;
+// }
+// @mixin transform-style($style) {
+// -webkit-transform-style: $style;
+// -moz-transform-style: $style;
+// -ms-transform-style: $style;
+// -o-transform-style: $style;
+// transform-style: $style;
+// }
+// @mixin rotate($deg:45deg){
+// @include transform(rotate($deg));
+// }
+// @mixin scale($factor:.5){
+// @include transform(scale($factor));
+// }
+// @mixin translate($x,$y){
+// @include transform(translate($x,$y));
+// }
+// @mixin translate3d($x,$y,$z) {
+// @include transform(translate3d($x,$y,$z));
+// }
+// @mixin translateHardware($x,$y) {
+// @include translate($x,$y);
+// -webkit-transform: translate3d($x,$y,0);
+// -moz-transform: translate3d($x,$y,0);
+// -o-transform: translate3d($x,$y,0);
+// -ms-transform: translate3d($x,$y,0);
+// transform: translate3d($x,$y,0);
+// }
+
+
+// Transitions
+
+// @mixin transition($args:200ms) {
+// -webkit-transition: $args;
+// -moz-transition: $args;
+// -o-transition: $args;
+// -ms-transition: $args;
+// transition: $args;
+// }
+// @mixin transition-delay($delay:0) {
+// -webkit-transition-delay: $delay;
+// -moz-transition-delay: $delay;
+// -o-transition-delay: $delay;
+// -ms-transition-delay: $delay;
+// transition-delay: $delay;
+// }
+// @mixin transition-duration($duration:200ms) {
+// -webkit-transition-duration: $duration;
+// -moz-transition-duration: $duration;
+// -o-transition-duration: $duration;
+// -ms-transition-duration: $duration;
+// transition-duration: $duration;
+// }
+// @mixin transition-property($property:all) {
+// -webkit-transition-property: $property;
+// -moz-transition-property: $property;
+// -o-transition-property: $property;
+// -ms-transition-property: $property;
+// transition-property: $property;
+// }
+// @mixin transition-timing-function($function:ease) {
+// -webkit-transition-timing-function: $function;
+// -moz-transition-timing-function: $function;
+// -o-transition-timing-function: $function;
+// -ms-transition-timing-function: $function;
+// transition-timing-function: $function;
+// }
diff --git a/docs/assets/css/components/_preloader.scss b/docs/assets/css/components/_preloader.scss
new file mode 100644
index 0000000..31e1600
--- /dev/null
+++ b/docs/assets/css/components/_preloader.scss
@@ -0,0 +1,334 @@
+/*
+ @license
+ Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
+ This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
+ The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
+ The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
+ Code distributed by Google as part of the polymer project is also
+ subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
+ */
+
+/**************************/
+/* STYLES FOR THE SPINNER */
+/**************************/
+
+/*
+ * Constants:
+ * STROKEWIDTH = 3px
+ * ARCSIZE = 270 degrees (amount of circle the arc takes up)
+ * ARCTIME = 1333ms (time it takes to expand and contract arc)
+ * ARCSTARTROT = 216 degrees (how much the start location of the arc
+ * should rotate each time, 216 gives us a
+ * 5 pointed star shape (it's 360/5 * 3).
+ * For a 7 pointed star, we might do
+ * 360/7 * 3 = 154.286)
+ * CONTAINERWIDTH = 28px
+ * SHRINK_TIME = 400ms
+ */
+
+
+.preloader-wrapper {
+ display: inline-block;
+ position: relative;
+ width: 48px;
+ height: 48px;
+
+ &.small {
+ width: 36px;
+ height: 36px;
+ }
+
+ &.big {
+ width: 64px;
+ height: 64px;
+ }
+
+ &.active {
+ /* duration: 360 * ARCTIME / (ARCSTARTROT + (360-ARCSIZE)) */
+ -webkit-animation: container-rotate 1568ms linear infinite;
+ animation: container-rotate 1568ms linear infinite;
+ }
+}
+
+@-webkit-keyframes container-rotate {
+ to { -webkit-transform: rotate(360deg) }
+}
+
+@keyframes container-rotate {
+ to { transform: rotate(360deg) }
+}
+
+.spinner-layer {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ border-color: $spinner-default-color;
+}
+
+.spinner-blue,
+.spinner-blue-only {
+ border-color: #4285f4;
+}
+
+.spinner-red,
+.spinner-red-only {
+ border-color: #db4437;
+}
+
+.spinner-yellow,
+.spinner-yellow-only {
+ border-color: #f4b400;
+}
+
+.spinner-green,
+.spinner-green-only {
+ border-color: #0f9d58;
+}
+
+/**
+ * IMPORTANT NOTE ABOUT CSS ANIMATION PROPERTIES (keanulee):
+ *
+ * iOS Safari (tested on iOS 8.1) does not handle animation-delay very well - it doesn't
+ * guarantee that the animation will start _exactly_ after that value. So we avoid using
+ * animation-delay and instead set custom keyframes for each color (as redundant as it
+ * seems).
+ *
+ * We write out each animation in full (instead of separating animation-name,
+ * animation-duration, etc.) because under the polyfill, Safari does not recognize those
+ * specific properties properly, treats them as -webkit-animation, and overrides the
+ * other animation rules. See https://github.com/Polymer/platform/issues/53.
+ */
+.active .spinner-layer.spinner-blue {
+ /* durations: 4 * ARCTIME */
+ -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, blue-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, blue-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+.active .spinner-layer.spinner-red {
+ /* durations: 4 * ARCTIME */
+ -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, red-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, red-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+.active .spinner-layer.spinner-yellow {
+ /* durations: 4 * ARCTIME */
+ -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, yellow-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, yellow-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+.active .spinner-layer.spinner-green {
+ /* durations: 4 * ARCTIME */
+ -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, green-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, green-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+.active .spinner-layer,
+.active .spinner-layer.spinner-blue-only,
+.active .spinner-layer.spinner-red-only,
+.active .spinner-layer.spinner-yellow-only,
+.active .spinner-layer.spinner-green-only {
+ /* durations: 4 * ARCTIME */
+ opacity: 1;
+ -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+@-webkit-keyframes fill-unfill-rotate {
+ 12.5% { -webkit-transform: rotate(135deg); } /* 0.5 * ARCSIZE */
+ 25% { -webkit-transform: rotate(270deg); } /* 1 * ARCSIZE */
+ 37.5% { -webkit-transform: rotate(405deg); } /* 1.5 * ARCSIZE */
+ 50% { -webkit-transform: rotate(540deg); } /* 2 * ARCSIZE */
+ 62.5% { -webkit-transform: rotate(675deg); } /* 2.5 * ARCSIZE */
+ 75% { -webkit-transform: rotate(810deg); } /* 3 * ARCSIZE */
+ 87.5% { -webkit-transform: rotate(945deg); } /* 3.5 * ARCSIZE */
+ to { -webkit-transform: rotate(1080deg); } /* 4 * ARCSIZE */
+}
+
+@keyframes fill-unfill-rotate {
+ 12.5% { transform: rotate(135deg); } /* 0.5 * ARCSIZE */
+ 25% { transform: rotate(270deg); } /* 1 * ARCSIZE */
+ 37.5% { transform: rotate(405deg); } /* 1.5 * ARCSIZE */
+ 50% { transform: rotate(540deg); } /* 2 * ARCSIZE */
+ 62.5% { transform: rotate(675deg); } /* 2.5 * ARCSIZE */
+ 75% { transform: rotate(810deg); } /* 3 * ARCSIZE */
+ 87.5% { transform: rotate(945deg); } /* 3.5 * ARCSIZE */
+ to { transform: rotate(1080deg); } /* 4 * ARCSIZE */
+}
+
+@-webkit-keyframes blue-fade-in-out {
+ from { opacity: 1; }
+ 25% { opacity: 1; }
+ 26% { opacity: 0; }
+ 89% { opacity: 0; }
+ 90% { opacity: 1; }
+ 100% { opacity: 1; }
+}
+
+@keyframes blue-fade-in-out {
+ from { opacity: 1; }
+ 25% { opacity: 1; }
+ 26% { opacity: 0; }
+ 89% { opacity: 0; }
+ 90% { opacity: 1; }
+ 100% { opacity: 1; }
+}
+
+@-webkit-keyframes red-fade-in-out {
+ from { opacity: 0; }
+ 15% { opacity: 0; }
+ 25% { opacity: 1; }
+ 50% { opacity: 1; }
+ 51% { opacity: 0; }
+}
+
+@keyframes red-fade-in-out {
+ from { opacity: 0; }
+ 15% { opacity: 0; }
+ 25% { opacity: 1; }
+ 50% { opacity: 1; }
+ 51% { opacity: 0; }
+}
+
+@-webkit-keyframes yellow-fade-in-out {
+ from { opacity: 0; }
+ 40% { opacity: 0; }
+ 50% { opacity: 1; }
+ 75% { opacity: 1; }
+ 76% { opacity: 0; }
+}
+
+@keyframes yellow-fade-in-out {
+ from { opacity: 0; }
+ 40% { opacity: 0; }
+ 50% { opacity: 1; }
+ 75% { opacity: 1; }
+ 76% { opacity: 0; }
+}
+
+@-webkit-keyframes green-fade-in-out {
+ from { opacity: 0; }
+ 65% { opacity: 0; }
+ 75% { opacity: 1; }
+ 90% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+@keyframes green-fade-in-out {
+ from { opacity: 0; }
+ 65% { opacity: 0; }
+ 75% { opacity: 1; }
+ 90% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+/**
+ * Patch the gap that appear between the two adjacent div.circle-clipper while the
+ * spinner is rotating (appears on Chrome 38, Safari 7.1, and IE 11).
+ */
+.gap-patch {
+ position: absolute;
+ top: 0;
+ left: 45%;
+ width: 10%;
+ height: 100%;
+ overflow: hidden;
+ border-color: inherit;
+}
+
+.gap-patch .circle {
+ width: 1000%;
+ left: -450%;
+}
+
+.circle-clipper {
+ display: inline-block;
+ position: relative;
+ width: 50%;
+ height: 100%;
+ overflow: hidden;
+ border-color: inherit;
+
+ .circle {
+ width: 200%;
+ height: 100%;
+ border-width: 3px; /* STROKEWIDTH */
+ border-style: solid;
+ border-color: inherit;
+ border-bottom-color: transparent !important;
+ border-radius: 50%;
+ -webkit-animation: none;
+ animation: none;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ }
+
+ &.left .circle {
+ left: 0;
+ border-right-color: transparent !important;
+ -webkit-transform: rotate(129deg);
+ transform: rotate(129deg);
+ }
+ &.right .circle {
+ left: -100%;
+ border-left-color: transparent !important;
+ -webkit-transform: rotate(-129deg);
+ transform: rotate(-129deg);
+ }
+}
+
+
+
+.active .circle-clipper.left .circle {
+ /* duration: ARCTIME */
+ -webkit-animation: left-spin 1333ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: left-spin 1333ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+.active .circle-clipper.right .circle {
+ /* duration: ARCTIME */
+ -webkit-animation: right-spin 1333ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: right-spin 1333ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+@-webkit-keyframes left-spin {
+ from { -webkit-transform: rotate(130deg); }
+ 50% { -webkit-transform: rotate(-5deg); }
+ to { -webkit-transform: rotate(130deg); }
+}
+
+@keyframes left-spin {
+ from { transform: rotate(130deg); }
+ 50% { transform: rotate(-5deg); }
+ to { transform: rotate(130deg); }
+}
+
+@-webkit-keyframes right-spin {
+ from { -webkit-transform: rotate(-130deg); }
+ 50% { -webkit-transform: rotate(5deg); }
+ to { -webkit-transform: rotate(-130deg); }
+}
+
+@keyframes right-spin {
+ from { transform: rotate(-130deg); }
+ 50% { transform: rotate(5deg); }
+ to { transform: rotate(-130deg); }
+}
+
+#spinnerContainer.cooldown {
+ /* duration: SHRINK_TIME */
+ -webkit-animation: container-rotate 1568ms linear infinite, fade-out 400ms cubic-bezier(0.4, 0.0, 0.2, 1);
+ animation: container-rotate 1568ms linear infinite, fade-out 400ms cubic-bezier(0.4, 0.0, 0.2, 1);
+}
+
+@-webkit-keyframes fade-out {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+
+@keyframes fade-out {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
\ No newline at end of file
diff --git a/docs/assets/css/components/_roboto.scss b/docs/assets/css/components/_roboto.scss
new file mode 100644
index 0000000..741ab5b
--- /dev/null
+++ b/docs/assets/css/components/_roboto.scss
@@ -0,0 +1,49 @@
+@font-face {
+ font-family: "Roboto";
+ src: local(Roboto Thin), url('#{$roboto-font-path}Roboto-Thin.eot');
+ src: url("#{$roboto-font-path}Roboto-Thin.eot?#iefix") format('embedded-opentype'),
+ url("#{$roboto-font-path}Roboto-Thin.woff2") format("woff2"),
+ url("#{$roboto-font-path}Roboto-Thin.woff") format("woff"),
+ url("#{$roboto-font-path}Roboto-Thin.ttf") format("truetype");
+
+ font-weight: 200;
+}
+@font-face {
+ font-family: "Roboto";
+ src: local(Roboto Light), url('#{$roboto-font-path}Roboto-Light.eot');
+ src: url("#{$roboto-font-path}Roboto-Light.eot?#iefix") format('embedded-opentype'),
+ url("#{$roboto-font-path}Roboto-Light.woff2") format("woff2"),
+ url("#{$roboto-font-path}Roboto-Light.woff") format("woff"),
+ url("#{$roboto-font-path}Roboto-Light.ttf") format("truetype");
+ font-weight: 300;
+}
+
+@font-face {
+ font-family: "Roboto";
+ src: local(Roboto Regular), url('#{$roboto-font-path}Roboto-Regular.eot');
+ src: url("#{$roboto-font-path}Roboto-Regular.eot?#iefix") format('embedded-opentype'),
+ url("#{$roboto-font-path}Roboto-Regular.woff2") format("woff2"),
+ url("#{$roboto-font-path}Roboto-Regular.woff") format("woff"),
+ url("#{$roboto-font-path}Roboto-Regular.ttf") format("truetype");
+ font-weight: 400;
+}
+
+@font-face {
+ font-family: "Roboto";
+ src: url('#{$roboto-font-path}Roboto-Medium.eot');
+ src: url("#{$roboto-font-path}Roboto-Medium.eot?#iefix") format('embedded-opentype'),
+ url("#{$roboto-font-path}Roboto-Medium.woff2") format("woff2"),
+ url("#{$roboto-font-path}Roboto-Medium.woff") format("woff"),
+ url("#{$roboto-font-path}Roboto-Medium.ttf") format("truetype");
+ font-weight: 500;
+}
+
+@font-face {
+ font-family: "Roboto";
+ src: url('#{$roboto-font-path}Roboto-Bold.eot');
+ src: url("#{$roboto-font-path}Roboto-Bold.eot?#iefix") format('embedded-opentype'),
+ url("#{$roboto-font-path}Roboto-Bold.woff2") format("woff2"),
+ url("#{$roboto-font-path}Roboto-Bold.woff") format("woff"),
+ url("#{$roboto-font-path}Roboto-Bold.ttf") format("truetype");
+ font-weight: 700;
+}
diff --git a/docs/assets/css/components/_sideNav.scss b/docs/assets/css/components/_sideNav.scss
new file mode 100644
index 0000000..db3e4dc
--- /dev/null
+++ b/docs/assets/css/components/_sideNav.scss
@@ -0,0 +1,133 @@
+.side-nav {
+ position: fixed;
+ width: 240px;
+ left: 0;
+ top: 0;
+ margin: 0;
+ transform: translateX(-100%);
+ height: 100%;
+ height: calc(100% + 60px);
+ height: -moz-calc(100%); //Temporary Firefox Fix
+ padding-bottom: 60px;
+ background-color: $sidenav-bg-color;
+ z-index: 999;
+ backface-visibility: hidden;
+ overflow-y: auto;
+ will-change: transform;
+ backface-visibility: hidden;
+ transform: translateX(-105%);
+
+ @extend .z-depth-1;
+
+ // Right Align
+ &.right-aligned {
+ right: 0;
+ transform: translateX(105%);
+ left: auto;
+ transform: translateX(100%);
+ }
+
+ .collapsible {
+ margin: 0;
+ }
+
+
+ li {
+ float: none;
+ line-height: $sidenav-item-height;
+
+ &.active { background-color: rgba(0,0,0,.05); }
+ }
+
+ a {
+ color: $sidenav-font-color;
+ display: block;
+ font-size: 1rem;
+ height: $sidenav-item-height;
+ line-height: $sidenav-item-height;
+ padding: 0 $sidenav-padding-right;
+
+ &:hover { background-color: rgba(0,0,0,.05);}
+
+ &.btn, &.btn-large, &.btn-flat, &.btn-floating {
+ margin: 10px 15px;
+ }
+
+ &.btn,
+ &.btn-large,
+ &.btn-floating { color: $button-raised-color; }
+ &.btn-flat { color: $button-flat-color; }
+
+ &.btn:hover,
+ &.btn-large:hover { background-color: lighten($button-raised-background, 5%); }
+ &.btn-floating:hover { background-color: $button-raised-background; }
+ }
+}
+
+
+// Touch interaction
+.drag-target {
+ height: 100%;
+ width: 10px;
+ position: fixed;
+ top: 0;
+ z-index: 998;
+}
+
+
+// Hidden side-nav for all sizes
+.side-nav.fixed {
+ a {
+ display: block;
+ padding: 0 $sidenav-padding-right;
+ color: $sidenav-font-color;
+ }
+}
+
+
+// Fixed side-nav shown
+.side-nav.fixed {
+ left: 0;
+ transform: translateX(0);
+ position: fixed;
+
+ // Right Align
+ &.right-aligned {
+ right: 0;
+ left: auto;
+ }
+}
+
+// Fixed sideNav hide on smaller
+@media #{$medium-and-down} {
+ .side-nav.fixed {
+ transform: translateX(-105%);
+
+ &.right-aligned {
+ transform: translateX(105%);
+ }
+ }
+}
+
+
+.side-nav .collapsible-body li.active,
+.side-nav.fixed .collapsible-body li.active {
+ background-color: $primary-color;
+ a {
+ color: $sidenav-bg-color;
+ }
+}
+
+
+#sidenav-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+
+ height: 120vh;
+ background-color: rgba(0,0,0,.5);
+ z-index: 997;
+
+ will-change: opacity;
+}
diff --git a/docs/assets/css/components/_slider.scss b/docs/assets/css/components/_slider.scss
new file mode 100644
index 0000000..2265cdb
--- /dev/null
+++ b/docs/assets/css/components/_slider.scss
@@ -0,0 +1,92 @@
+.slider {
+ position: relative;
+ height: 400px;
+ width: 100%;
+
+ // Fullscreen slider
+ &.fullscreen {
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+
+ ul.slides {
+ height: 100%;
+ }
+
+ ul.indicators {
+ z-index: 2;
+ bottom: 30px;
+ }
+ }
+
+ .slides {
+ background-color: $slider-bg-color;
+ margin: 0;
+ height: 400px;
+
+ li {
+ opacity: 0;
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ width: 100%;
+ height: inherit;
+ overflow: hidden;
+
+ img {
+ height: 100%;
+ width: 100%;
+ background-size: cover;
+ background-position: center;
+ }
+
+ .caption {
+ color: #fff;
+ position: absolute;
+ top: 15%;
+ left: 15%;
+ width: 70%;
+ opacity: 0;
+
+ p { color: $slider-bg-color-light; }
+ }
+
+ &.active {
+ z-index: 2;
+ }
+ }
+ }
+
+
+ .indicators {
+ position: absolute;
+ text-align: center;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ margin: 0;
+
+ .indicator-item {
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ height: 16px;
+ width: 16px;
+ margin: 0 12px;
+ background-color: $slider-bg-color-light;
+
+ transition: background-color .3s;
+ border-radius: 50%;
+
+ &.active {
+ background-color: $slider-indicator-color;
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/docs/assets/css/components/_table_of_contents.scss b/docs/assets/css/components/_table_of_contents.scss
new file mode 100644
index 0000000..2872bdb
--- /dev/null
+++ b/docs/assets/css/components/_table_of_contents.scss
@@ -0,0 +1,33 @@
+/***************
+ Nav List
+***************/
+.table-of-contents {
+ &.fixed {
+ position: fixed;
+ }
+
+ li {
+ padding: 2px 0;
+ }
+ a {
+ display: inline-block;
+ font-weight: 300;
+ color: #757575;
+ padding-left: 20px;
+ height: 1.5rem;
+ line-height: 1.5rem;
+ letter-spacing: .4;
+ display: inline-block;
+
+ &:hover {
+ color: lighten(#757575, 20%);
+ padding-left: 19px;
+ border-left: 1px solid lighten(color("materialize-red", "base"),10%);
+ }
+ &.active {
+ font-weight: 500;
+ padding-left: 18px;
+ border-left: 2px solid lighten(color("materialize-red", "base"),10%);
+ }
+ }
+}
diff --git a/docs/assets/css/components/_tabs.scss b/docs/assets/css/components/_tabs.scss
new file mode 100644
index 0000000..6380882
--- /dev/null
+++ b/docs/assets/css/components/_tabs.scss
@@ -0,0 +1,56 @@
+.tabs {
+ display: flex;
+ position: relative;
+ overflow-x: auto;
+ overflow-y: hidden;
+ height: 48px;
+ background-color: $tabs-bg-color;
+ margin: 0 auto;
+ width: 100%;
+ white-space: nowrap;
+
+ .tab {
+ -webkit-box-flex: 1;
+ -webkit-flex-grow: 1;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+ display: block;
+ float: left;
+ text-align: center;
+ line-height: 48px;
+ height: 48px;
+ padding: 0;
+ margin: 0;
+ text-transform: uppercase;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ letter-spacing: .8px;
+ width: 15%;
+ min-width: 80px;
+
+ a {
+ color: $tabs-text-color;
+ display: block;
+ width: 100%;
+ height: 100%;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ transition: color .28s ease;
+ &:hover {
+ color: lighten($tabs-text-color, 20%);
+ }
+ }
+
+ &.disabled a {
+ color: lighten($tabs-text-color, 20%);
+ cursor: default;
+ }
+ }
+ .indicator {
+ position: absolute;
+ bottom: 0;
+ height: 2px;
+ background-color: $tabs-underline-color;
+ will-change: left, right;
+ }
+}
diff --git a/docs/assets/css/components/_toast.scss b/docs/assets/css/components/_toast.scss
new file mode 100644
index 0000000..7d4ef90
--- /dev/null
+++ b/docs/assets/css/components/_toast.scss
@@ -0,0 +1,65 @@
+#toast-container {
+ display:block;
+ position: fixed;
+ z-index: 10000;
+
+ @media #{$small-and-down} {
+ min-width: 100%;
+ bottom: 0%;
+ }
+ @media #{$medium-only} {
+ left: 5%;
+ bottom: 7%;
+ max-width: 90%;
+ }
+ @media #{$large-and-up} {
+ top: 10%;
+ right: 7%;
+ max-width: 86%;
+ }
+}
+
+.toast {
+ @extend .z-depth-1;
+ border-radius: 2px;
+ top: 0;
+ width: auto;
+ clear: both;
+ margin-top: 10px;
+ position: relative;
+ max-width:100%;
+ height: auto;
+ min-height: $toast-height;
+ line-height: 1.5em;
+ word-break: break-all;
+ background-color: $toast-color;
+ padding: 10px 25px;
+ font-size: 1.1rem;
+ font-weight: 300;
+ color: $toast-text-color;
+
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .btn, .btn-flat {
+ margin: 0;
+ margin-left: 3rem;
+ }
+
+ &.rounded{
+ border-radius: 24px;
+ }
+
+ @media #{$small-and-down} {
+ width:100%;
+ border-radius: 0;
+ }
+ @media #{$medium-only} {
+ float: left;
+ }
+ @media #{$large-and-up} {
+ float: right;
+ }
+
+}
diff --git a/docs/assets/css/components/_tooltip.scss b/docs/assets/css/components/_tooltip.scss
new file mode 100644
index 0000000..81d8fbf
--- /dev/null
+++ b/docs/assets/css/components/_tooltip.scss
@@ -0,0 +1,34 @@
+.material-tooltip {
+ padding: 10px 8px;
+ font-size: 1rem;
+ z-index: 2000;
+ background-color: transparent;
+ border-radius: 2px;
+ color: #fff;
+ min-height: 36px;
+ line-height: 120%;
+ opacity: 0;
+ display: none;
+ position: absolute;
+ text-align: center;
+ max-width: calc(100% - 4px);
+ overflow: hidden;
+ left:0;
+ top:0;
+ pointer-events: none;
+ will-change: top, left;
+}
+
+.backdrop {
+ position: absolute;
+ opacity: 0;
+ display: none;
+ height: 7px;
+ width: 14px;
+ border-radius: 0 0 14px 14px;
+ background-color: #323232;
+ z-index: -1;
+ transform-origin: 50% 10%;
+
+ will-change: transform, opacity;
+}
diff --git a/docs/assets/css/components/_typography.scss b/docs/assets/css/components/_typography.scss
new file mode 100644
index 0000000..5301c80
--- /dev/null
+++ b/docs/assets/css/components/_typography.scss
@@ -0,0 +1,61 @@
+
+a {
+ text-decoration: none;
+}
+
+html{
+ line-height: 1.5;
+
+ @media only screen and (min-width: 0) {
+ font-size: 14px;
+ }
+
+ @media only screen and (min-width: $medium-screen) {
+ font-size: 14.5px;
+ }
+
+ @media only screen and (min-width: $large-screen) {
+ font-size: 15px;
+ }
+
+ font-family: "Roboto", sans-serif;
+ font-weight: normal;
+ color: $off-black;
+}
+h1, h2, h3, h4, h5, h6 {
+ font-weight: 400;
+ line-height: 1.1;
+}
+
+// Header Styles
+h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { font-weight: inherit; }
+h1 { font-size: $h1-fontsize; line-height: 110%; margin: ($h1-fontsize / 2) 0 ($h1-fontsize / 2.5) 0;}
+h2 { font-size: $h2-fontsize; line-height: 110%; margin: ($h2-fontsize / 2) 0 ($h2-fontsize / 2.5) 0;}
+h3 { font-size: $h3-fontsize; line-height: 110%; margin: ($h3-fontsize / 2) 0 ($h3-fontsize / 2.5) 0;}
+h4 { font-size: $h4-fontsize; line-height: 110%; margin: ($h4-fontsize / 2) 0 ($h4-fontsize / 2.5) 0;}
+h5 { font-size: $h5-fontsize; line-height: 110%; margin: ($h5-fontsize / 2) 0 ($h5-fontsize / 2.5) 0;}
+h6 { font-size: $h6-fontsize; line-height: 110%; margin: ($h6-fontsize / 2) 0 ($h6-fontsize / 2.5) 0;}
+
+// Text Styles
+em { font-style: italic; }
+strong { font-weight: 500; }
+small { font-size: 75%; }
+.light { font-weight: 300; }
+.thin { font-weight: 200; }
+
+
+.flow-text{
+ font-weight: 300;
+ $i: 0;
+ @while $i <= $intervals {
+ @media only screen and (min-width : 360 + ($i * $interval-size)) {
+ font-size: 1.2rem * (1 + (.02 * $i));
+ }
+ $i: $i + 1;
+ }
+
+ // Handle below 360px screen
+ @media only screen and (max-width: 360px) {
+ font-size: 1.2rem;
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/css/components/_variables.scss b/docs/assets/css/components/_variables.scss
new file mode 100644
index 0000000..7f47653
--- /dev/null
+++ b/docs/assets/css/components/_variables.scss
@@ -0,0 +1,309 @@
+/* ==========================================================================
+ Materialize variables
+ ========================================================================== */
+/**
+ * Table of Contents:
+ *
+ * 1. Colors
+ * 2. Badges
+ * 3. Buttons
+ * 4. Cards
+ * 5. Collapsible
+ * 6. Chips
+ * 7. Date Picker
+ * 8. Dropdown
+ * 10. Forms
+ * 11. Global
+ * 12. Grid
+ * 13. Navigation Bar
+ * 14. Side Navigation
+ * 15. Photo Slider
+ * 16. Spinners | Loaders
+ * 17. Tabs
+ * 18. Tables
+ * 19. Toasts
+ * 20. Typography
+ * 21. Footer
+ * 22. Flow Text
+ * 23. Collections
+ * 24. Progress Bar
+ */
+
+
+/* 1. Colors
+ ========================================================================== */
+
+$primary-color: color("materialize-red", "lighten-2") !default;
+$primary-color-light: lighten($primary-color, 15%) !default;
+$primary-color-dark: darken($primary-color, 15%) !default;
+
+$secondary-color: color("teal", "lighten-1") !default;
+$success-color: color("green", "base") !default;
+$error-color: color("red", "base") !default;
+$link-color: color("light-blue", "darken-1") !default;
+
+
+/* 2. Badges
+ ========================================================================== */
+
+$badge-bg-color: $secondary-color !default;
+
+
+/* 3. Buttons
+ ========================================================================== */
+
+// Shared styles
+$button-border: none !default;
+$button-background-focus: lighten($secondary-color, 4%) !default;
+$button-font-size: 1.3rem !default;
+$button-height: 36px !default;
+$button-padding: 0 2rem !default;
+$button-radius: 2px !default;
+
+// Disabled styles
+$button-disabled-background: #DFDFDF !default;
+$button-disabled-color: #9F9F9F !default;
+
+// Raised buttons
+$button-raised-background: $secondary-color !default;
+$button-raised-background-hover: lighten($button-raised-background, 5%) !default;
+$button-raised-color: #fff !default;
+
+// Large buttons
+$button-large-icon-font-size: 1.6rem !default;
+$button-large-height: $button-height * 1.5 !default;
+
+// Flat buttons
+$button-flat-color: #343434 !default;
+$button-flat-disabled-color: lighten(#999, 10%) !default;
+
+// Floating buttons
+$button-floating-background: $secondary-color !default;
+$button-floating-background-hover: $button-floating-background !default;
+$button-floating-color: #fff !default;
+$button-floating-size: 37px !default;
+$button-floating-large-size: $button-floating-size * 1.5 !default;
+$button-floating-radius: 50% !default;
+
+
+/* 4. Cards
+ ========================================================================== */
+
+$card-padding: 20px !default;
+$card-bg-color: #fff !default;
+$card-link-color: color("orange", "accent-2") !default;
+$card-link-color-light: lighten($card-link-color, 20%) !default;
+
+
+/* 5. Collapsible
+ ========================================================================== */
+
+$collapsible-height: 3rem !default;
+$collapsible-header-color: #fff !default;
+$collapsible-border-color: #ddd !default;
+
+
+/* 6. Chips
+ ========================================================================== */
+
+$chip-bg-color: #e4e4e4 !default;
+
+
+/* 7. Date Picker
+ ========================================================================== */
+
+$datepicker-weekday-bg: darken($secondary_color, 7%) !default;
+$datepicker-date-bg: $secondary_color !default;
+$datepicker-year: rgba(255, 255, 255, .4) !default;
+$datepicker-focus: rgba(0,0,0, .05) !default;
+$datepicker-selected: $secondary-color !default;
+$datepicker-selected-outfocus: desaturate(lighten($secondary-color, 35%), 15%) !default;
+
+
+/* 8. Dropdown
+ ========================================================================== */
+
+$dropdown-bg-color: #fff !default;
+$dropdown-hover-bg-color: #eee !default;
+$dropdown-color: $secondary-color !default;
+$dropdown-item-height: 50px !default;
+
+
+/* 9. Fonts
+ ========================================================================== */
+
+$roboto-font-path: "../fonts/roboto/" !default;
+
+
+/* 10. Forms
+ ========================================================================== */
+
+// Text Inputs + Textarea
+$input-height: 3rem !default;
+$input-border-color: color("grey", "base") !default;
+$input-border: 1px solid $input-border-color !default;
+$input-background: #fff !default;
+$input-error-color: $error-color !default;
+$input-success-color: $success-color !default;
+$input-focus-color: $secondary-color !default;
+$input-font-size: 1rem !default;
+$input-margin: 0 0 15px 0 !default;
+$input-padding: 0 !default;
+$input-transition: all .3s !default;
+$label-font-size: .8rem !default;
+$input-disabled-color: rgba(0,0,0, .26) !default;
+$input-disabled-solid-color: #BDBDBD !default;
+$input-disabled-border: 1px dotted $input-disabled-color !default;
+$input-invalid-border: 1px solid $input-error-color !default;
+$placeholder-text-color: lighten($input-border-color, 20%) !default;
+
+// Radio Buttons
+$radio-fill-color: $secondary-color !default;
+$radio-empty-color: #5a5a5a !default;
+$radio-border: 2px solid $radio-fill-color !default;
+
+// Range
+$range-height: 14px !default;
+$range-width: 14px !default;
+$track-height: 3px !default;
+
+// Select
+$select-border: 1px solid #f2f2f2 !default;
+$select-background: rgba(255, 255, 255, 0.90) !default;
+$select-focus: 1px solid lighten($secondary-color, 47%) !default;
+$select-padding: 5px !default;
+$select-radius: 2px !default;
+$select-disabled-color: rgba(0,0,0,.3) !default;
+
+// Switches
+$switch-bg-color: $secondary-color !default;
+$switch-checked-lever-bg: desaturate(lighten($secondary-color, 25%), 25%) !default;
+$switch-unchecked-bg: #F1F1F1 !default;
+$switch-unchecked-lever-bg: #818181 !default;
+$switch-radius: 15px !default;
+
+
+/* 11. Global
+ ========================================================================== */
+
+// Media Query Ranges
+$small-screen-up: 601px !default;
+$medium-screen-up: 993px !default;
+$large-screen-up: 1201px !default;
+$small-screen: 600px !default;
+$medium-screen: 992px !default;
+$large-screen: 1200px !default;
+
+$medium-and-up: "only screen and (min-width : #{$small-screen-up})" !default;
+$large-and-up: "only screen and (min-width : #{$medium-screen-up})" !default;
+$small-and-down: "only screen and (max-width : #{$small-screen})" !default;
+$medium-and-down: "only screen and (max-width : #{$medium-screen})" !default;
+$medium-only: "only screen and (min-width : #{$small-screen-up}) and (max-width : #{$medium-screen})" !default;
+
+
+/* 12. Grid
+ ========================================================================== */
+
+$num-cols: 12 !default;
+$gutter-width: 1.5rem !default;
+$element-top-margin: $gutter-width/3 !default;
+$element-bottom-margin: ($gutter-width*2)/3 !default;
+
+
+/* 13. Navigation Bar
+ ========================================================================== */
+
+$navbar-height: 64px !default;
+$navbar-height-mobile: 56px !default;
+$navbar-font-color: #fff !default;
+$navbar-brand-font-size: 2.1rem !default;
+
+
+/* 14. Side Navigation
+ ========================================================================== */
+
+$sidenav-font-color: #444 !default;
+$sidenav-bg-color: #fff !default;
+$sidenav-padding-right: 30px !default;
+$sidenav-item-height: 64px !default;
+
+
+/* 15. Photo Slider
+ ========================================================================== */
+
+$slider-bg-color: color('grey', 'base') !default;
+$slider-bg-color-light: color('grey', 'lighten-2') !default;
+$slider-indicator-color: color('green', 'base') !default;
+
+
+/* 16. Spinners | Loaders
+ ========================================================================== */
+
+$spinner-default-color: $secondary-color !default;
+
+
+/* 17. Tabs
+ ========================================================================== */
+
+$tabs-underline-color: $primary-color-light !default;
+$tabs-text-color: $primary-color !default;
+$tabs-bg-color: #fff !default;
+
+
+/* 18. Tables
+ ========================================================================== */
+
+$table-border-color: #d0d0d0 !default;
+$table-striped-color: #f2f2f2 !default;
+
+
+/* 19. Toasts
+ ========================================================================== */
+
+$toast-height: 48px !default;
+$toast-color: #323232 !default;
+$toast-text-color: #fff !default;
+
+
+/* 20. Typography
+ ========================================================================== */
+
+$off-black: rgba(0, 0, 0, 0.87) !default;
+// Header Styles
+$h1-fontsize: 4.2rem !default;
+$h2-fontsize: 3.56rem !default;
+$h3-fontsize: 2.92rem !default;
+$h4-fontsize: 2.28rem !default;
+$h5-fontsize: 1.64rem !default;
+$h6-fontsize: 1rem !default;
+
+
+/* 21. Footer
+ ========================================================================== */
+
+$footer-bg-color: $primary-color !default;
+
+
+/* 22. Flow Text
+ ========================================================================== */
+
+$range : $large-screen - $small-screen !default;
+$intervals: 20 !default;
+$interval-size: $range / $intervals !default;
+
+
+/* 23. Collections
+ ========================================================================== */
+
+$collection-border-color: #e0e0e0 !default;
+$collection-bg-color: #fff !default;
+$collection-active-bg-color: $secondary-color !default;
+$collection-active-color: lighten($secondary-color, 55%) !default;
+$collection-hover-bg-color: #ddd !default;
+$collection-link-color: $secondary-color !default;
+
+
+/* 24. Progress Bar
+ ========================================================================== */
+
+$progress-bar-color: $secondary-color !default;
diff --git a/docs/assets/css/components/_waves.scss b/docs/assets/css/components/_waves.scss
new file mode 100644
index 0000000..4991711
--- /dev/null
+++ b/docs/assets/css/components/_waves.scss
@@ -0,0 +1,173 @@
+
+/*!
+ * Waves v0.6.0
+ * http://fian.my.id/Waves
+ *
+ * Copyright 2014 Alfiana E. Sibuea and other contributors
+ * Released under the MIT license
+ * https://github.com/fians/Waves/blob/master/LICENSE
+ */
+
+
+.waves-effect {
+ position: relative;
+ cursor: pointer;
+ display: inline-block;
+ overflow: hidden;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ // white-space: nowrap;
+ // outline: 0;
+
+ vertical-align: middle;
+ // cursor: pointer;
+ // border: none;
+ // outline: none;
+ // color: inherit;
+ // background-color: rgba(0, 0, 0, 0);
+ // font-size: 1em;
+ // line-height:1em;
+ // text-align: center;
+ // text-decoration: none;
+ z-index: 1;
+ will-change: opacity, transform;
+ transition: all .3s ease-out;
+
+ .waves-ripple {
+ position: absolute;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ margin-top:-10px;
+ margin-left:-10px;
+ opacity: 0;
+
+ background: rgba(0,0,0,0.2);
+ // $gradient: rgba(0,0,0,0.2) 0,rgba(0,0,0,.3) 40%,rgba(0,0,0,.4) 50%,rgba(0,0,0,.5) 60%,rgba(255,255,255,0) 70%;
+ // background: -webkit-radial-gradient($gradient);
+ // background: -o-radial-gradient($gradient);
+ // background: -moz-radial-gradient($gradient);
+ // background: radial-gradient($gradient);
+ transition: all 0.7s ease-out;
+ transition-property: transform, opacity;
+ transform: scale(0);
+ pointer-events: none;
+ }
+
+ // Waves Colors
+ &.waves-light .waves-ripple {
+ background-color: rgba(255, 255, 255, 0.45);
+ }
+
+ &.waves-red .waves-ripple {
+ background-color: rgba(244, 67, 54, .70);
+ }
+ &.waves-yellow .waves-ripple {
+ background-color: rgba(255, 235, 59, .70);
+ }
+ &.waves-orange .waves-ripple {
+ background-color: rgba(255, 152, 0, .70);
+ }
+ &.waves-purple .waves-ripple {
+ background-color: rgba(156, 39, 176, 0.70);
+ }
+ &.waves-green .waves-ripple {
+ background-color: rgba(76, 175, 80, 0.70);
+ }
+ &.waves-teal .waves-ripple {
+ background-color: rgba(0, 150, 136, 0.70);
+ }
+
+ // Style input button bug.
+ input[type="button"], input[type="reset"], input[type="submit"] {
+ border: 0;
+ font-style: normal;
+ font-size: inherit;
+ text-transform: inherit;
+ background: none;
+ }
+
+}
+
+.waves-notransition {
+ transition: none #{"!important"};
+}
+
+.waves-circle {
+ transform: translateZ(0);
+ -webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%);
+}
+
+// .waves-button,
+// .waves-button:hover,
+// .waves-button:visited,
+// .waves-button-input {
+// white-space: nowrap;
+// vertical-align: middle;
+// cursor: pointer;
+// border: none;
+// outline: none;
+// color: inherit;
+// background-color: rgba(0, 0, 0, 0);
+// font-size: 1em;
+// line-height:1em;
+// text-align: center;
+// text-decoration: none;
+// z-index: 1;
+// }
+
+// .waves-button {
+// padding: 0.85em 1.1em;
+// border-radius: 0.2em;
+// }
+
+// .waves-button-input {
+// margin: 0;
+// padding: 0.85em 1.1em;
+// }
+
+.waves-input-wrapper {
+ border-radius: 0.2em;
+ vertical-align: bottom;
+
+ // &.waves-button {
+ // padding: 0;
+ // }
+
+ .waves-button-input {
+ position: relative;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ }
+}
+
+.waves-circle {
+ text-align: center;
+ width: 2.5em;
+ height: 2.5em;
+ line-height: 2.5em;
+ border-radius: 50%;
+ -webkit-mask-image: none;
+}
+
+// .waves-float {
+ // -webkit-mask-image: none;
+ // @include box-shadow(0px 1px 1.5px 1px rgba(0, 0, 0, 0.12));
+
+ // &:active {
+ // @include box-shadow(0px 8px 20px 1px rgba(0, 0, 0, 0.30));
+// }
+// }
+
+.waves-block {
+ display: block;
+}
+
+/* Firefox Bug: link not triggered */
+a.waves-effect .waves-ripple {
+ z-index: -1;
+}
\ No newline at end of file
diff --git a/docs/assets/css/components/date_picker/_default.date.scss b/docs/assets/css/components/date_picker/_default.date.scss
new file mode 100644
index 0000000..84e96b5
--- /dev/null
+++ b/docs/assets/css/components/date_picker/_default.date.scss
@@ -0,0 +1,435 @@
+/* ==========================================================================
+ $BASE-DATE-PICKER
+ ========================================================================== */
+/**
+ * The picker box.
+ */
+.picker__box {
+ padding: 0 1em;
+}
+/**
+ * The header containing the month and year stuff.
+ */
+.picker__header {
+ text-align: center;
+ position: relative;
+ margin-top: .75em;
+}
+/**
+ * The month and year labels.
+ */
+.picker__month,
+.picker__year {
+// font-weight: 500;
+ display: inline-block;
+ margin-left: .25em;
+ margin-right: .25em;
+}
+/**
+ * The month and year selectors.
+ */
+.picker__select--month,
+.picker__select--year {
+
+ height: 2em;
+ padding: 0;
+ margin-left: .25em;
+ margin-right: .25em;
+}
+
+// Modified
+.picker__select--month.browser-default {
+ display: inline;
+ background-color: #FFFFFF;
+ width: 40%;
+}
+.picker__select--year.browser-default {
+ display: inline;
+ background-color: #FFFFFF;
+ width: 26%;
+}
+.picker__select--month:focus,
+.picker__select--year:focus {
+ border-color: $datepicker-focus;
+}
+/**
+ * The month navigation buttons.
+ */
+.picker__nav--prev,
+.picker__nav--next {
+ position: absolute;
+ padding: .5em 1.25em;
+ width: 1em;
+ height: 1em;
+ box-sizing: content-box;
+ top: -0.25em;
+}
+//@media (min-width: 24.5em) {
+// .picker__nav--prev,
+// .picker__nav--next {
+// top: -0.33em;
+// }
+//}
+.picker__nav--prev {
+ left: -1em;
+ padding-right: 1.25em;
+}
+//@media (min-width: 24.5em) {
+// .picker__nav--prev {
+// padding-right: 1.5em;
+// }
+//}
+.picker__nav--next {
+ right: -1em;
+ padding-left: 1.25em;
+}
+//@media (min-width: 24.5em) {
+// .picker__nav--next {
+// padding-left: 1.5em;
+// }
+//}
+
+.picker__nav--disabled,
+.picker__nav--disabled:hover,
+.picker__nav--disabled:before,
+.picker__nav--disabled:before:hover {
+ cursor: default;
+ background: none;
+ border-right-color: #f5f5f5;
+ border-left-color: #f5f5f5;
+}
+/**
+ * The calendar table of dates
+ */
+.picker__table {
+ text-align: center;
+ border-collapse: collapse;
+ border-spacing: 0;
+ table-layout: fixed;
+ font-size: 1rem;
+ width: 100%;
+ margin-top: .75em;
+ margin-bottom: .5em;
+}
+
+
+
+.picker__table th, .picker__table td {
+ text-align: center;
+}
+
+
+
+
+
+
+.picker__table td {
+ margin: 0;
+ padding: 0;
+}
+/**
+ * The weekday labels
+ */
+.picker__weekday {
+ width: 14.285714286%;
+ font-size: .75em;
+ padding-bottom: .25em;
+ color: #999999;
+ font-weight: 500;
+ /* Increase the spacing a tad */
+}
+@media (min-height: 33.875em) {
+ .picker__weekday {
+ padding-bottom: .5em;
+ }
+}
+/**
+ * The days on the calendar
+ */
+
+.picker__day--today {
+ position: relative;
+ color: #595959;
+ letter-spacing: -.3;
+ padding: .75rem 0;
+ font-weight: 400;
+ border: 1px solid transparent;
+
+}
+
+//.picker__day--today:before {
+// content: " ";
+// position: absolute;
+// top: 2px;
+// right: 2px;
+// width: 0;
+// height: 0;
+// border-top: 0.5em solid #0059bc;
+// border-left: .5em solid transparent;
+//}
+.picker__day--disabled:before {
+ border-top-color: #aaaaaa;
+}
+
+
+.picker__day--infocus:hover{
+ cursor: pointer;
+ color: #000;
+ font-weight: 500;
+}
+
+.picker__day--outfocus {
+ display: none;
+ padding: .75rem 0;
+ color: #fff;
+
+}
+.picker__day--outfocus:hover {
+ cursor: pointer;
+ color: #dddddd;
+// background: #b1dcfb;
+ font-weight: 500;
+}
+
+
+.picker__day--highlighted {
+// border-color: #0089ec;
+}
+.picker__day--highlighted:hover,
+.picker--focused .picker__day--highlighted {
+ cursor: pointer;
+// color: #000000;
+// background: #b1dcfb;
+// font-weight: 500;
+}
+.picker__day--selected,
+.picker__day--selected:hover,
+.picker--focused .picker__day--selected {
+
+
+// Circle background
+ border-radius: 50%;
+ transform: scale(.75);
+ background: #0089ec;
+ color: #ffffff;
+}
+.picker__day--disabled,
+.picker__day--disabled:hover,
+.picker--focused .picker__day--disabled {
+ background: #f5f5f5;
+ border-color: #f5f5f5;
+ color: #dddddd;
+ cursor: default;
+}
+.picker__day--highlighted.picker__day--disabled,
+.picker__day--highlighted.picker__day--disabled:hover {
+ background: #bbbbbb;
+}
+/**
+ * The footer containing the "today", "clear", and "close" buttons.
+ */
+.picker__footer {
+ text-align: center;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+.picker__button--today,
+.picker__button--clear,
+.picker__button--close {
+ border: 1px solid #ffffff;
+ background: #ffffff;
+ font-size: .8em;
+ padding: .66em 0;
+ font-weight: bold;
+ width: 33%;
+ display: inline-block;
+ vertical-align: bottom;
+}
+.picker__button--today:hover,
+.picker__button--clear:hover,
+.picker__button--close:hover {
+ cursor: pointer;
+ color: #000000;
+ background: #b1dcfb;
+ border-bottom-color: #b1dcfb;
+}
+.picker__button--today:focus,
+.picker__button--clear:focus,
+.picker__button--close:focus {
+ background: #b1dcfb;
+ border-color: $datepicker-focus;
+ outline: none;
+}
+.picker__button--today:before,
+.picker__button--clear:before,
+.picker__button--close:before {
+ position: relative;
+ display: inline-block;
+ height: 0;
+}
+.picker__button--today:before,
+.picker__button--clear:before {
+ content: " ";
+ margin-right: .45em;
+}
+.picker__button--today:before {
+ top: -0.05em;
+ width: 0;
+ border-top: 0.66em solid #0059bc;
+ border-left: .66em solid transparent;
+}
+.picker__button--clear:before {
+ top: -0.25em;
+ width: .66em;
+ border-top: 3px solid #ee2200;
+}
+.picker__button--close:before {
+ content: "\D7";
+ top: -0.1em;
+ vertical-align: top;
+ font-size: 1.1em;
+ margin-right: .35em;
+ color: #777777;
+}
+.picker__button--today[disabled],
+.picker__button--today[disabled]:hover {
+ background: #f5f5f5;
+ border-color: #f5f5f5;
+ color: #dddddd;
+ cursor: default;
+}
+.picker__button--today[disabled]:before {
+ border-top-color: #aaaaaa;
+}
+
+/* ==========================================================================
+ CUSTOM MATERIALIZE STYLES
+ ========================================================================== */
+.picker__box {
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+.picker__date-display {
+ text-align: center;
+ background-color: $datepicker-date-bg;
+ color: #fff;
+ padding-bottom: 15px;
+ font-weight: 300;
+}
+
+.picker__nav--prev:hover,
+.picker__nav--next:hover {
+ cursor: pointer;
+ color: #000000;
+ background: $datepicker-selected-outfocus;
+}
+
+.picker__weekday-display {
+ background-color: $datepicker-weekday-bg;
+ padding: 10px;
+ font-weight: 200;
+ letter-spacing: .5;
+ font-size: 1rem;
+ margin-bottom: 15px;
+}
+
+.picker__month-display {
+ text-transform: uppercase;
+ font-size: 2rem;
+}
+.picker__day-display {
+
+ font-size: 4.5rem;
+ font-weight: 400;
+}
+.picker__year-display {
+ font-size: 1.8rem;
+ color: $datepicker-year;
+}
+
+.picker__box {
+ padding: 0;
+}
+.picker__calendar-container {
+ padding: 0 1rem;
+
+ thead {
+ border: none;
+ }
+}
+
+// Calendar
+.picker__table {
+ margin-top: 0;
+ margin-bottom: .5em;
+}
+
+.picker__day--infocus {
+ color: #595959;
+ letter-spacing: -.3;
+ padding: .75rem 0;
+ font-weight: 400;
+ border: 1px solid transparent;
+}
+
+//Today style
+.picker__day.picker__day--today {
+ color: $datepicker-selected;
+}
+
+.picker__day.picker__day--today.picker__day--selected {
+ color: #fff;
+}
+
+// Table Header
+.picker__weekday {
+ font-size: .9rem;
+}
+
+
+.picker__day--selected,
+.picker__day--selected:hover,
+.picker--focused .picker__day--selected {
+ // Circle background
+ border-radius: 50%;
+ transform: scale(.9);
+ background-color: $datepicker-selected;
+ &.picker__day--outfocus {
+ background-color: $datepicker-selected-outfocus;
+ }
+ color: #ffffff;
+}
+
+.picker__footer {
+ text-align: right;
+ padding: 5px 10px;
+}
+
+// Materialize modified
+.picker__close, .picker__today {
+ font-size: 1.1rem;
+ padding: 0 1rem;
+ color: $datepicker-selected;
+}
+
+//month nav buttons
+.picker__nav--prev:before,
+.picker__nav--next:before {
+ content: " ";
+ border-top: .5em solid transparent;
+ border-bottom: .5em solid transparent;
+ border-right: 0.75em solid #676767;
+ width: 0;
+ height: 0;
+ display: block;
+ margin: 0 auto;
+}
+.picker__nav--next:before {
+ border-right: 0;
+ border-left: 0.75em solid #676767;
+}
+button.picker__today:focus, button.picker__clear:focus, button.picker__close:focus {
+ background-color: $datepicker-selected-outfocus;
+}
diff --git a/docs/assets/css/components/date_picker/_default.scss b/docs/assets/css/components/date_picker/_default.scss
new file mode 100644
index 0000000..971fbc7
--- /dev/null
+++ b/docs/assets/css/components/date_picker/_default.scss
@@ -0,0 +1,201 @@
+/* ==========================================================================
+ $BASE-PICKER
+ ========================================================================== */
+/**
+ * Note: the root picker element should *NOT* be styled more than what's here.
+ */
+.picker {
+ font-size: 16px;
+ text-align: left;
+ line-height: 1.2;
+ color: #000000;
+ position: absolute;
+ z-index: 10000;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+/**
+ * The picker input element.
+ */
+.picker__input {
+ cursor: default;
+}
+/**
+ * When the picker is opened, the input element is "activated".
+ */
+.picker__input.picker__input--active {
+ border-color: #0089ec;
+}
+/**
+ * The holder is the only "scrollable" top-level container element.
+ */
+.picker__holder {
+ width: 100%;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+/*!
+ * Default mobile-first, responsive styling for pickadate.js
+ * Demo: http://amsul.github.io/pickadate.js
+ */
+/**
+ * Note: the root picker element should *NOT* be styled more than what's here.
+ */
+/**
+ * Make the holder and frame fullscreen.
+ */
+.picker__holder,
+.picker__frame {
+ bottom: 0;
+ left: 0;
+ right: 0;
+ top: 100%;
+}
+/**
+ * The holder should overlay the entire screen.
+ */
+.picker__holder {
+ position: fixed;
+ -webkit-transition: background 0.15s ease-out, top 0s 0.15s;
+ -moz-transition: background 0.15s ease-out, top 0s 0.15s;
+ transition: background 0.15s ease-out, top 0s 0.15s;
+ -webkit-backface-visibility: hidden;
+}
+/**
+ * The frame that bounds the box contents of the picker.
+ */
+.picker__frame {
+ position: absolute;
+ margin: 0 auto;
+ min-width: 256px;
+
+// picker width
+ width: 300px;
+ max-height: 350px;
+
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
+ filter: alpha(opacity=0);
+ -moz-opacity: 0;
+ opacity: 0;
+ -webkit-transition: all 0.15s ease-out;
+ -moz-transition: all 0.15s ease-out;
+ transition: all 0.15s ease-out;
+}
+@media (min-height: 28.875em) {
+ .picker__frame {
+ overflow: visible;
+ top: auto;
+ bottom: -100%;
+ max-height: 80%;
+ }
+}
+@media (min-height: 40.125em) {
+ .picker__frame {
+ margin-bottom: 7.5%;
+ }
+}
+/**
+ * The wrapper sets the stage to vertically align the box contents.
+ */
+.picker__wrap {
+ display: table;
+ width: 100%;
+ height: 100%;
+}
+@media (min-height: 28.875em) {
+ .picker__wrap {
+ display: block;
+ }
+}
+/**
+ * The box contains all the picker contents.
+ */
+.picker__box {
+ background: #ffffff;
+ display: table-cell;
+ vertical-align: middle;
+}
+//@media (min-height: 26.5em) {
+// .picker__box {
+//// font-size: 1.25em;
+// }
+//}
+@media (min-height: 28.875em) {
+ .picker__box {
+ display: block;
+
+// picker header font-size
+// font-size: 1rem;
+
+ border: 1px solid #777777;
+ border-top-color: #898989;
+ border-bottom-width: 0;
+ -webkit-border-radius: 5px 5px 0 0;
+ -moz-border-radius: 5px 5px 0 0;
+ border-radius: 5px 5px 0 0;
+ -webkit-box-shadow: 0 12px 36px 16px rgba(0, 0, 0, 0.24);
+ -moz-box-shadow: 0 12px 36px 16px rgba(0, 0, 0, 0.24);
+ box-shadow: 0 12px 36px 16px rgba(0, 0, 0, 0.24);
+ }
+}
+//@media (min-height: 40.125em) {
+// .picker__box {
+// font-size: 1.1rem;
+// border-bottom-width: 1px;
+// -webkit-border-radius: 5px;
+// -moz-border-radius: 5px;
+// border-radius: 5px;
+// }
+//}
+/**
+ * When the picker opens...
+ */
+.picker--opened .picker__holder {
+ top: 0;
+ background: transparent;
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#1E000000,endColorstr=#1E000000)";
+ zoom: 1;
+ background: rgba(0, 0, 0, 0.32);
+ -webkit-transition: background 0.15s ease-out;
+ -moz-transition: background 0.15s ease-out;
+ transition: background 0.15s ease-out;
+}
+.picker--opened .picker__frame {
+ top: 0;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
+ filter: alpha(opacity=100);
+ -moz-opacity: 1;
+ opacity: 1;
+}
+@media (min-height: 35.875em) {
+ .picker--opened .picker__frame {
+ top: 10%;
+ bottom: auto;
+ }
+}
+/**
+ * For `large` screens, transform into an inline picker.
+ */
+
+/* ==========================================================================
+ CUSTOM MATERIALIZE STYLES
+ ========================================================================== */
+
+.picker__input.picker__input--active {
+ border-color: color("blue", "lighten-5");
+}
+
+.picker__frame {
+ margin: 0 auto;
+ max-width: 325px;
+}
+
+@media (min-height: 38.875em) {
+ .picker--opened .picker__frame {
+ top: 10%;
+ bottom: auto;
+ }
+}
diff --git a/docs/assets/css/components/date_picker/_default.time.scss b/docs/assets/css/components/date_picker/_default.time.scss
new file mode 100644
index 0000000..0b159c8
--- /dev/null
+++ b/docs/assets/css/components/date_picker/_default.time.scss
@@ -0,0 +1,125 @@
+/* ==========================================================================
+ $BASE-TIME-PICKER
+ ========================================================================== */
+/**
+ * The list of times.
+ */
+.picker__list {
+ list-style: none;
+ padding: 0.75em 0 4.2em;
+ margin: 0;
+}
+/**
+ * The times on the clock.
+ */
+.picker__list-item {
+ border-bottom: 1px solid #dddddd;
+ border-top: 1px solid #dddddd;
+ margin-bottom: -1px;
+ position: relative;
+ background: #ffffff;
+ padding: .75em 1.25em;
+}
+@media (min-height: 46.75em) {
+ .picker__list-item {
+ padding: .5em 1em;
+ }
+}
+/* Hovered time */
+.picker__list-item:hover {
+ cursor: pointer;
+ color: #000000;
+ background: #b1dcfb;
+ border-color: #0089ec;
+ z-index: 10;
+}
+/* Highlighted and hovered/focused time */
+.picker__list-item--highlighted {
+ border-color: #0089ec;
+ z-index: 10;
+}
+.picker__list-item--highlighted:hover,
+.picker--focused .picker__list-item--highlighted {
+ cursor: pointer;
+ color: #000000;
+ background: #b1dcfb;
+}
+/* Selected and hovered/focused time */
+.picker__list-item--selected,
+.picker__list-item--selected:hover,
+.picker--focused .picker__list-item--selected {
+ background: #0089ec;
+ color: #ffffff;
+ z-index: 10;
+}
+/* Disabled time */
+.picker__list-item--disabled,
+.picker__list-item--disabled:hover,
+.picker--focused .picker__list-item--disabled {
+ background: #f5f5f5;
+ border-color: #f5f5f5;
+ color: #dddddd;
+ cursor: default;
+ border-color: #dddddd;
+ z-index: auto;
+}
+/**
+ * The clear button
+ */
+.picker--time .picker__button--clear {
+ display: block;
+ width: 80%;
+ margin: 1em auto 0;
+ padding: 1em 1.25em;
+ background: none;
+ border: 0;
+ font-weight: 500;
+ font-size: .67em;
+ text-align: center;
+ text-transform: uppercase;
+ color: #666;
+}
+.picker--time .picker__button--clear:hover,
+.picker--time .picker__button--clear:focus {
+ color: #000000;
+ background: #b1dcfb;
+ background: #ee2200;
+ border-color: #ee2200;
+ cursor: pointer;
+ color: #ffffff;
+ outline: none;
+}
+.picker--time .picker__button--clear:before {
+ top: -0.25em;
+ color: #666;
+ font-size: 1.25em;
+ font-weight: bold;
+}
+.picker--time .picker__button--clear:hover:before,
+.picker--time .picker__button--clear:focus:before {
+ color: #ffffff;
+}
+
+/* ==========================================================================
+ $DEFAULT-TIME-PICKER
+ ========================================================================== */
+/**
+ * The frame the bounds the time picker.
+ */
+.picker--time .picker__frame {
+ min-width: 256px;
+ max-width: 320px;
+}
+/**
+ * The picker box.
+ */
+.picker--time .picker__box {
+ font-size: 1em;
+ background: #f2f2f2;
+ padding: 0;
+}
+@media (min-height: 40.125em) {
+ .picker--time .picker__box {
+ margin-bottom: 5em;
+ }
+}
diff --git a/docs/assets/css/components/forms/_checkboxes.scss b/docs/assets/css/components/forms/_checkboxes.scss
new file mode 100644
index 0000000..a8743c1
--- /dev/null
+++ b/docs/assets/css/components/forms/_checkboxes.scss
@@ -0,0 +1,220 @@
+/* Checkboxes
+ ========================================================================== */
+
+/* CUSTOM CSS CHECKBOXES */
+form p {
+ margin-bottom: 10px;
+ text-align: left;
+}
+
+form p:last-child {
+ margin-bottom: 0;
+}
+
+/* Remove default checkbox */
+[type="checkbox"]:not(:checked),
+[type="checkbox"]:checked {
+ position: absolute;
+ left: -9999px;
+ opacity: 0;
+}
+
+// Checkbox Styles
+[type="checkbox"] {
+ // Text Label Style
+ + label {
+ position: relative;
+ padding-left: 35px;
+ cursor: pointer;
+ display: inline-block;
+ height: 25px;
+ line-height: 25px;
+ font-size: 1rem;
+
+ -webkit-user-select: none; /* webkit (safari, chrome) browsers */
+ -moz-user-select: none; /* mozilla browsers */
+ -khtml-user-select: none; /* webkit (konqueror) browsers */
+ -ms-user-select: none; /* IE10+ */
+ }
+
+ /* checkbox aspect */
+ + label:before,
+ &:not(.filled-in) + label:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 18px;
+ height: 18px;
+ z-index: 0;
+ border: 2px solid $radio-empty-color;
+ border-radius: 1px;
+ margin-top: 2px;
+ transition: .2s;
+ }
+
+ &:not(.filled-in) + label:after {
+ border: 0;
+ transform: scale(0);
+ }
+
+ &:not(:checked):disabled + label:before {
+ border: none;
+ background-color: $input-disabled-color;
+ }
+
+ // Focused styles
+ &.tabbed:focus + label:after {
+ transform: scale(1);
+ border: 0;
+ border-radius: 50%;
+ box-shadow: 0 0 0 10px rgba(0,0,0,.1);
+ background-color: rgba(0,0,0,.1);
+ }
+}
+
+[type="checkbox"]:checked {
+ + label:before {
+ top: -4px;
+ left: -5px;
+ width: 12px;
+ height: 22px;
+ border-top: 2px solid transparent;
+ border-left: 2px solid transparent;
+ border-right: $radio-border;
+ border-bottom: $radio-border;
+ transform: rotate(40deg);
+ backface-visibility: hidden;
+ transform-origin: 100% 100%;
+ }
+
+ &:disabled + label:before {
+ border-right: 2px solid $input-disabled-color;
+ border-bottom: 2px solid $input-disabled-color;
+ }
+}
+
+/* Indeterminate checkbox */
+[type="checkbox"]:indeterminate {
+ +label:before {
+ top: -11px;
+ left: -12px;
+ width: 10px;
+ height: 22px;
+ border-top: none;
+ border-left: none;
+ border-right: $radio-border;
+ border-bottom: none;
+ transform: rotate(90deg);
+ backface-visibility: hidden;
+ transform-origin: 100% 100%;
+ }
+
+ // Disabled indeterminate
+ &:disabled + label:before {
+ border-right: 2px solid $input-disabled-color;
+ background-color: transparent;
+ }
+}
+
+// Filled in Style
+[type="checkbox"].filled-in {
+ // General
+ + label:after {
+ border-radius: 2px;
+ }
+
+ + label:before,
+ + label:after {
+ content: '';
+ left: 0;
+ position: absolute;
+ /* .1s delay is for check animation */
+ transition: border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;
+ z-index: 1;
+ }
+
+ // Unchecked style
+ &:not(:checked) + label:before {
+ width: 0;
+ height: 0;
+ border: 3px solid transparent;
+ left: 6px;
+ top: 10px;
+
+ -webkit-transform: rotateZ(37deg);
+ transform: rotateZ(37deg);
+ -webkit-transform-origin: 20% 40%;
+ transform-origin: 100% 100%;
+ }
+
+ &:not(:checked) + label:after {
+ height: 20px;
+ width: 20px;
+ background-color: transparent;
+ border: 2px solid $radio-empty-color;
+ top: 0px;
+ z-index: 0;
+ }
+
+ // Checked style
+ &:checked {
+ + label:before {
+ top: 0;
+ left: 1px;
+ width: 8px;
+ height: 13px;
+ border-top: 2px solid transparent;
+ border-left: 2px solid transparent;
+ border-right: 2px solid $input-background;
+ border-bottom: 2px solid $input-background;
+ -webkit-transform: rotateZ(37deg);
+ transform: rotateZ(37deg);
+
+ -webkit-transform-origin: 100% 100%;
+ transform-origin: 100% 100%;
+ }
+
+ + label:after {
+ top: 0;
+ width: 20px;
+ height: 20px;
+ border: 2px solid $secondary-color;
+ background-color: $secondary-color;
+ z-index: 0;
+ }
+ }
+
+ // Focused styles
+ &.tabbed:focus + label:after {
+ border-radius: 2px;
+ border-color: $radio-empty-color;
+ background-color: rgba(0,0,0,.1);
+ }
+
+ &.tabbed:checked:focus + label:after {
+ border-radius: 2px;
+ background-color: $secondary-color;
+ border-color: $secondary-color;
+ }
+
+ // Disabled style
+ &:disabled:not(:checked) + label:before {
+ background-color: transparent;
+ border: 2px solid transparent;
+ }
+
+ &:disabled:not(:checked) + label:after {
+ border-color: transparent;
+ background-color: $input-disabled-solid-color;
+ }
+
+ &:disabled:checked + label:before {
+ background-color: transparent;
+ }
+
+ &:disabled:checked + label:after {
+ background-color: $input-disabled-solid-color;
+ border-color: $input-disabled-solid-color;
+ }
+}
diff --git a/docs/assets/css/components/forms/_file-input.scss b/docs/assets/css/components/forms/_file-input.scss
new file mode 100644
index 0000000..3b3fe55
--- /dev/null
+++ b/docs/assets/css/components/forms/_file-input.scss
@@ -0,0 +1,38 @@
+/* File Input
+ ========================================================================== */
+
+.file-field {
+ position: relative;
+
+ .file-path-wrapper {
+ overflow: hidden;
+ padding-left: 10px;
+ }
+
+ input.file-path { width: 100%; }
+
+ .btn {
+ float: left;
+ height: $input-height;
+ line-height: $input-height;
+ }
+
+ span {
+ cursor: pointer;
+ }
+
+ input[type=file] {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ font-size: 20px;
+ cursor: pointer;
+ opacity: 0;
+ filter: alpha(opacity=0);
+ }
+}
diff --git a/docs/assets/css/components/forms/_forms.scss b/docs/assets/css/components/forms/_forms.scss
new file mode 100644
index 0000000..e9b65b4
--- /dev/null
+++ b/docs/assets/css/components/forms/_forms.scss
@@ -0,0 +1,22 @@
+// Remove Focus Boxes
+select:focus {
+ outline: $select-focus;
+}
+
+button:focus {
+ outline: none;
+ background-color: $button-background-focus;
+}
+
+label {
+ font-size: $label-font-size;
+ color: $input-border-color;
+}
+
+@import 'input-fields';
+@import 'radio-buttons';
+@import 'checkboxes';
+@import 'switches';
+@import 'select';
+@import 'file-input';
+@import 'range';
diff --git a/docs/assets/css/components/forms/_input-fields.scss b/docs/assets/css/components/forms/_input-fields.scss
new file mode 100644
index 0000000..ebaf5ac
--- /dev/null
+++ b/docs/assets/css/components/forms/_input-fields.scss
@@ -0,0 +1,241 @@
+/* Text Inputs + Textarea
+ ========================================================================== */
+
+/* Style Placeholders */
+
+::-webkit-input-placeholder {
+ color: $placeholder-text-color;
+}
+
+:-moz-placeholder { /* Firefox 18- */
+ color: $placeholder-text-color;
+}
+
+::-moz-placeholder { /* Firefox 19+ */
+ color: $placeholder-text-color;
+}
+
+:-ms-input-placeholder {
+ color: $placeholder-text-color;
+}
+
+/* Text inputs */
+
+input:not([type]),
+input[type=text],
+input[type=password],
+input[type=email],
+input[type=url],
+input[type=time],
+input[type=date],
+input[type=datetime],
+input[type=datetime-local],
+input[type=tel],
+input[type=number],
+input[type=search],
+textarea.materialize-textarea {
+
+ // General Styles
+ background-color: transparent;
+ border: none;
+ border-bottom: $input-border;
+ border-radius: 0;
+ outline: none;
+ height: $input-height;
+ width: 100%;
+ font-size: $input-font-size;
+ margin: $input-margin;
+ padding: $input-padding;
+ box-shadow: none;
+ box-sizing: content-box;
+ transition: $input-transition;
+
+ // Disabled input style
+ &:disabled,
+ &[readonly="readonly"] {
+ color: $input-disabled-color;
+ border-bottom: $input-disabled-border;
+ }
+
+ // Disabled label style
+ &:disabled+label,
+ &[readonly="readonly"]+label {
+ color: $input-disabled-color;
+ }
+
+ // Focused input style
+ &:focus:not([readonly]) {
+ border-bottom: 1px solid $input-focus-color;
+ box-shadow: 0 1px 0 0 $input-focus-color;
+ }
+
+ // Focused label style
+ &:focus:not([readonly])+label {
+ color: $input-focus-color;
+ }
+
+ // Valid Input Style
+ &.valid,
+ &:focus.valid {
+ border-bottom: 1px solid $input-success-color;
+ box-shadow: 0 1px 0 0 $input-success-color;
+ }
+
+ // Custom Success Message
+ &.valid + label:after,
+ &:focus.valid + label:after {
+ content: attr(data-success);
+ color: $input-success-color;
+ opacity: 1;
+ }
+
+ // Invalid Input Style
+ &.invalid,
+ &:focus.invalid {
+ border-bottom: $input-invalid-border;
+ box-shadow: 0 1px 0 0 $input-error-color;
+ }
+
+ // Custom Error message
+ &.invalid + label:after,
+ &:focus.invalid + label:after {
+ content: attr(data-error);
+ color: $input-error-color;
+ opacity: 1;
+ }
+
+ // Full width label when using validate for error messages
+ &.validate + label {
+ width: 100%;
+ pointer-events: none;
+ }
+
+ // Form Message Shared Styles
+ & + label:after {
+ display: block;
+ content: "";
+ position: absolute;
+ top: 65px;
+ opacity: 0;
+ transition: .2s opacity ease-out, .2s color ease-out;
+ }
+}
+
+// Styling for input field wrapper
+.input-field {
+ position: relative;
+ margin-top: 1rem;
+
+ label {
+ color: $input-border-color;
+ position: absolute;
+ top: 0.8rem;
+ left: $gutter-width / 2;
+ font-size: 1rem;
+ cursor: text;
+ transition: .2s ease-out;
+ }
+
+ label.active {
+ font-size: $label-font-size;
+ transform: translateY(-140%);
+ }
+
+ // Prefix Icons
+ .prefix {
+ position: absolute;
+ width: $input-height;
+ font-size: 2rem;
+ transition: color .2s;
+
+ &.active { color: $input-focus-color; }
+ }
+
+ .prefix ~ input,
+ .prefix ~ textarea {
+ margin-left: 3rem;
+ width: 92%;
+ width: calc(100% - 3rem);
+ }
+
+ .prefix ~ textarea { padding-top: .8rem; }
+ .prefix ~ label { margin-left: 3rem; }
+
+ @media #{$medium-and-down} {
+ .prefix ~ input {
+ width: 86%;
+ width: calc(100% - 3rem);
+ }
+ }
+
+ @media #{$small-and-down} {
+ .prefix ~ input {
+ width: 80%;
+ width: calc(100% - 3rem);
+ }
+ }
+}
+
+
+/* Search Field */
+
+.input-field input[type=search] {
+ display: block;
+ line-height: inherit;
+ padding-left: 4rem;
+ width: calc(100% - 4rem);
+
+ &:focus {
+ background-color: $input-background;
+ border: 0;
+ box-shadow: none;
+ color: #444;
+
+ & + label i,
+ & ~ .mdi-navigation-close,
+ & ~ .material-icons {
+ color: #444;
+ }
+ }
+
+ & + label {
+ left: 1rem;
+ }
+
+ & ~ .mdi-navigation-close,
+ & ~ .material-icons {
+ position: absolute;
+ top: 0;
+ right: 1rem;
+ color: transparent;
+ cursor: pointer;
+ font-size: 2rem;
+ transition: .3s color;
+ }
+}
+
+
+/* Textarea */
+
+// Default textarea
+textarea {
+ width: 100%;
+ height: $input-height;
+ background-color: transparent;
+
+ &.materialize-textarea {
+ overflow-y: hidden; /* prevents scroll bar flash */
+ padding: 1.6rem 0; /* prevents text jump on Enter keypress */
+ resize: none;
+ min-height: $input-height;
+ }
+}
+
+// For textarea autoresize
+.hiddendiv {
+ display: none;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow-wrap: break-word; /* future version of deprecated 'word-wrap' */
+ padding-top: 1.2rem; /* prevents text jump on Enter keypress */
+}
diff --git a/docs/assets/css/components/forms/_radio-buttons.scss b/docs/assets/css/components/forms/_radio-buttons.scss
new file mode 100644
index 0000000..cb1f37d
--- /dev/null
+++ b/docs/assets/css/components/forms/_radio-buttons.scss
@@ -0,0 +1,119 @@
+/* Radio Buttons
+ ========================================================================== */
+
+// Remove default Radio Buttons
+[type="radio"]:not(:checked),
+[type="radio"]:checked {
+ position: absolute;
+ left: -9999px;
+ opacity: 0;
+}
+
+[type="radio"]:not(:checked) + label,
+[type="radio"]:checked + label {
+ position: relative;
+ padding-left: 35px;
+ cursor: pointer;
+ display: inline-block;
+ height: 25px;
+ line-height: 25px;
+ font-size: 1rem;
+ transition: .28s ease;
+
+ -khtml-user-select: none; /* webkit (konqueror) browsers */
+ user-select: none;
+}
+
+[type="radio"] + label:before,
+[type="radio"] + label:after {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ margin: 4px;
+ width: 16px;
+ height: 16px;
+ z-index: 0;
+ transition: .28s ease;
+}
+
+/* Unchecked styles */
+[type="radio"]:not(:checked) + label:before,
+[type="radio"]:not(:checked) + label:after,
+[type="radio"]:checked + label:before,
+[type="radio"]:checked + label:after,
+[type="radio"].with-gap:checked + label:before,
+[type="radio"].with-gap:checked + label:after {
+ border-radius: 50%;
+}
+
+[type="radio"]:not(:checked) + label:before,
+[type="radio"]:not(:checked) + label:after {
+ border: 2px solid $radio-empty-color;
+}
+
+[type="radio"]:not(:checked) + label:after {
+ z-index: -1;
+ transform: scale(0);
+}
+
+/* Checked styles */
+[type="radio"]:checked + label:before {
+ border: 2px solid transparent;
+}
+
+[type="radio"]:checked + label:after,
+[type="radio"].with-gap:checked + label:before,
+[type="radio"].with-gap:checked + label:after {
+ border: $radio-border;
+}
+
+[type="radio"]:checked + label:after,
+[type="radio"].with-gap:checked + label:after {
+ background-color: $radio-fill-color;
+ z-index: 0;
+}
+
+[type="radio"]:checked + label:after {
+ transform: scale(1.02);
+}
+
+/* Radio With gap */
+[type="radio"].with-gap:checked + label:after {
+ transform: scale(.5);
+}
+
+/* Focused styles */
+[type="radio"].tabbed:focus + label:before {
+ box-shadow: 0 0 0 10px rgba(0,0,0,.1);
+}
+
+/* Disabled Radio With gap */
+[type="radio"].with-gap:disabled:checked + label:before {
+ border: 2px solid $input-disabled-color;
+}
+
+[type="radio"].with-gap:disabled:checked + label:after {
+ border: none;
+ background-color: $input-disabled-color;
+}
+
+/* Disabled style */
+[type="radio"]:disabled:not(:checked) + label:before,
+[type="radio"]:disabled:checked + label:before {
+ background-color: transparent;
+ border-color: $input-disabled-color;
+}
+
+[type="radio"]:disabled + label {
+ color: $input-disabled-color;
+}
+
+[type="radio"]:disabled:not(:checked) + label:before {
+ border-color: $input-disabled-color;
+}
+
+[type="radio"]:disabled:checked + label:after {
+ background-color: $input-disabled-color;
+ border-color: $input-disabled-solid-color;
+}
diff --git a/docs/assets/css/components/forms/_range.scss b/docs/assets/css/components/forms/_range.scss
new file mode 100644
index 0000000..7947375
--- /dev/null
+++ b/docs/assets/css/components/forms/_range.scss
@@ -0,0 +1,159 @@
+/* Range
+ ========================================================================== */
+
+.range-field {
+ position: relative;
+}
+
+input[type=range],
+input[type=range] + .thumb {
+ @extend .no-select;
+ cursor: pointer;
+}
+
+input[type=range] {
+ position: relative;
+ background-color: transparent;
+ border: none;
+ outline: none;
+ width: 100%;
+ margin: 15px 0;
+ padding: 0;
+
+ &:focus {
+ outline: none;
+ }
+}
+
+input[type=range] + .thumb {
+ position: absolute;
+ border: none;
+ height: 0;
+ width: 0;
+ border-radius: 50%;
+ background-color: $radio-fill-color;
+ top: 10px;
+ margin-left: -6px;
+
+ transform-origin: 50% 50%;
+ transform: rotate(-45deg);
+
+ .value {
+ display: block;
+ width: 30px;
+ text-align: center;
+ color: $radio-fill-color;
+ font-size: 0;
+ transform: rotate(45deg);
+ }
+
+ &.active {
+ border-radius: 50% 50% 50% 0;
+
+ .value {
+ color: $input-background;
+ margin-left: -1px;
+ margin-top: 8px;
+ font-size: 10px;
+ }
+ }
+}
+
+// WebKit
+input[type=range] {
+ -webkit-appearance: none;
+}
+
+input[type=range]::-webkit-slider-runnable-track {
+ height: $track-height;
+ background: #c2c0c2;
+ border: none;
+}
+
+input[type=range]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ border: none;
+ height: $range-height;
+ width: $range-width;
+ border-radius: 50%;
+ background-color: $radio-fill-color;
+ transform-origin: 50% 50%;
+ margin: -5px 0 0 0;
+ transition: .3s;
+}
+
+input[type=range]:focus::-webkit-slider-runnable-track {
+ background: #ccc;
+}
+
+// FireFox
+input[type=range] {
+ /* fix for FF unable to apply focus style bug */
+ border: 1px solid white;
+
+ /*required for proper track sizing in FF*/
+}
+
+input[type=range]::-moz-range-track {
+ height: $track-height;
+ background: #ddd;
+ border: none;
+}
+
+input[type=range]::-moz-range-thumb {
+ border: none;
+ height: $range-height;
+ width: $range-width;
+ border-radius: 50%;
+ background: $radio-fill-color;
+ margin-top: -5px;
+}
+
+// hide the outline behind the border
+input[type=range]:-moz-focusring {
+ outline: 1px solid #fff;
+ outline-offset: -1px;
+}
+
+input[type=range]:focus::-moz-range-track {
+ background: #ccc;
+}
+
+// IE 10+
+input[type=range]::-ms-track {
+ height: $track-height;
+
+ // remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead
+ background: transparent;
+
+ // leave room for the larger thumb to overflow with a transparent border */
+ border-color: transparent;
+ border-width: 6px 0;
+
+ /*remove default tick marks*/
+ color: transparent;
+}
+
+input[type=range]::-ms-fill-lower {
+ background: #777;
+}
+
+input[type=range]::-ms-fill-upper {
+ background: #ddd;
+}
+
+input[type=range]::-ms-thumb {
+ border: none;
+ height: $range-height;
+ width: $range-width;
+ border-radius: 50%;
+ background: $radio-fill-color;
+}
+
+input[type=range]:focus::-ms-fill-lower {
+ background: #888;
+}
+
+input[type=range]:focus::-ms-fill-upper {
+ background: #ccc;
+}
diff --git a/docs/assets/css/components/forms/_select.scss b/docs/assets/css/components/forms/_select.scss
new file mode 100644
index 0000000..884351a
--- /dev/null
+++ b/docs/assets/css/components/forms/_select.scss
@@ -0,0 +1,116 @@
+/* Select Field
+ ========================================================================== */
+
+select { display: none; }
+select.browser-default { display: block; }
+
+select {
+ background-color: $select-background;
+ width: 100%;
+ padding: $select-padding;
+ border: $select-border;
+ border-radius: $select-radius;
+ height: $input-height;
+}
+
+.select-label {
+ position: absolute;
+}
+
+.select-wrapper {
+ position: relative;
+
+ input.select-dropdown {
+ position: relative;
+ cursor: pointer;
+ background-color: transparent;
+ border: none;
+ border-bottom: $input-border;
+ outline: none;
+ height: $input-height;
+ line-height: $input-height;
+ width: 100%;
+ font-size: $input-font-size;
+ margin: $input-margin;
+ padding: 0;
+ display: block;
+ }
+
+ span.caret {
+ color: initial;
+ position: absolute;
+ right: 0;
+ top: 16px;
+ font-size: 10px;
+ &.disabled {
+ color: $input-disabled-color;
+ }
+ }
+
+ & + label {
+ position: absolute;
+ top: -14px;
+ font-size: $label-font-size;
+ }
+}
+
+// Disabled styles
+select:disabled {
+ color: rgba(0,0,0,.3);
+}
+
+.select-wrapper input.select-dropdown:disabled {
+ color: rgba(0,0,0,.3);
+ cursor: default;
+ -webkit-user-select: none; /* webkit (safari, chrome) browsers */
+ -moz-user-select: none; /* mozilla browsers */
+ -ms-user-select: none; /* IE10+ */
+ border-bottom: 1px solid rgba(0,0,0,.3);
+}
+
+.select-wrapper i {
+ color: $select-disabled-color;
+}
+
+.select-dropdown li.disabled,
+.select-dropdown li.disabled > span,
+.select-dropdown li.optgroup {
+ color: $select-disabled-color;
+ background-color: transparent;
+}
+
+// Prefix Icons
+.prefix ~ .select-wrapper {
+ margin-left: 3rem;
+ width: 92%;
+ width: calc(100% - 3rem);
+}
+
+.prefix ~ label { margin-left: 3rem; }
+
+// Icons
+.select-dropdown li {
+ img {
+ height: $dropdown-item-height - 10;
+ width: $dropdown-item-height - 10;
+ margin: 5px 15px;
+ float: right;
+ }
+}
+
+// Optgroup styles
+.select-dropdown li.optgroup {
+ border-top: 1px solid $dropdown-hover-bg-color;
+
+ &.selected > span {
+ color: rgba(0, 0, 0, .7);
+ }
+
+ & > span {
+ color: rgba(0, 0, 0, .4);
+ }
+
+ & ~ li.optgroup-option {
+ padding-left: 1rem;
+ }
+}
diff --git a/docs/assets/css/components/forms/_switches.scss b/docs/assets/css/components/forms/_switches.scss
new file mode 100644
index 0000000..2c6655a
--- /dev/null
+++ b/docs/assets/css/components/forms/_switches.scss
@@ -0,0 +1,78 @@
+/* Switch
+ ========================================================================== */
+
+.switch,
+.switch * {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ -ms-user-select: none;
+}
+
+.switch label {
+ cursor: pointer;
+}
+
+.switch label input[type=checkbox] {
+ opacity: 0;
+ width: 0;
+ height: 0;
+
+ &:checked + .lever {
+ background-color: $switch-checked-lever-bg;
+
+ &:after {
+ background-color: $switch-bg-color;
+ left: 24px;
+ }
+ }
+}
+
+.switch label .lever {
+ content: "";
+ display: inline-block;
+ position: relative;
+ width: 40px;
+ height: 15px;
+ background-color: $switch-unchecked-lever-bg;
+ border-radius: $switch-radius;
+ margin-right: 10px;
+ transition: background 0.3s ease;
+ vertical-align: middle;
+ margin: 0 16px;
+
+ &:after {
+ content: "";
+ position: absolute;
+ display: inline-block;
+ width: 21px;
+ height: 21px;
+ background-color: $switch-unchecked-bg;
+ border-radius: 21px;
+ box-shadow: 0 1px 3px 1px rgba(0,0,0,.4);
+ left: -5px;
+ top: -3px;
+ transition: left 0.3s ease, background .3s ease, box-shadow 0.1s ease;
+ }
+}
+
+// Switch active style
+input[type=checkbox]:checked:not(:disabled) ~ .lever:active::after,
+input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::after {
+ box-shadow: 0 1px 3px 1px rgba(0,0,0,.4), 0 0 0 15px transparentize($switch-bg-color, .9);
+}
+
+input[type=checkbox]:not(:disabled) ~ .lever:active:after,
+input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::after {
+ box-shadow: 0 1px 3px 1px rgba(0,0,0,.4), 0 0 0 15px rgba(0, 0, 0, .08);
+}
+
+// Disabled Styles
+.switch input[type=checkbox][disabled] + .lever {
+ cursor: default;
+}
+
+.switch label input[type=checkbox][disabled] + .lever:after,
+.switch label input[type=checkbox][disabled]:checked + .lever:after {
+ background-color: $input-disabled-solid-color;
+}
diff --git a/docs/assets/css/materialize.css b/docs/assets/css/materialize.css
new file mode 100644
index 0000000..5c7a474
--- /dev/null
+++ b/docs/assets/css/materialize.css
@@ -0,0 +1,11 @@
+.materialize-red{background-color:#e51c23 !important}.materialize-red-text{color:#e51c23 !important}.materialize-red.lighten-5{background-color:#fdeaeb !important}.materialize-red-text.text-lighten-5{color:#fdeaeb !important}.materialize-red.lighten-4{background-color:#f8c1c3 !important}.materialize-red-text.text-lighten-4{color:#f8c1c3 !important}.materialize-red.lighten-3{background-color:#f3989b !important}.materialize-red-text.text-lighten-3{color:#f3989b !important}.materialize-red.lighten-2{background-color:#ee6e73 !important}.materialize-red-text.text-lighten-2{color:#ee6e73 !important}.materialize-red.lighten-1{background-color:#ea454b !important}.materialize-red-text.text-lighten-1{color:#ea454b !important}.materialize-red.darken-1{background-color:#d0181e !important}.materialize-red-text.text-darken-1{color:#d0181e !important}.materialize-red.darken-2{background-color:#b9151b !important}.materialize-red-text.text-darken-2{color:#b9151b !important}.materialize-red.darken-3{background-color:#a21318 !important}.materialize-red-text.text-darken-3{color:#a21318 !important}.materialize-red.darken-4{background-color:#8b1014 !important}.materialize-red-text.text-darken-4{color:#8b1014 !important}.red{background-color:#F44336 !important}.red-text{color:#F44336 !important}.red.lighten-5{background-color:#FFEBEE !important}.red-text.text-lighten-5{color:#FFEBEE !important}.red.lighten-4{background-color:#FFCDD2 !important}.red-text.text-lighten-4{color:#FFCDD2 !important}.red.lighten-3{background-color:#EF9A9A !important}.red-text.text-lighten-3{color:#EF9A9A !important}.red.lighten-2{background-color:#E57373 !important}.red-text.text-lighten-2{color:#E57373 !important}.red.lighten-1{background-color:#EF5350 !important}.red-text.text-lighten-1{color:#EF5350 !important}.red.darken-1{background-color:#E53935 !important}.red-text.text-darken-1{color:#E53935 !important}.red.darken-2{background-color:#D32F2F !important}.red-text.text-darken-2{color:#D32F2F !important}.red.darken-3{background-color:#C62828 !important}.red-text.text-darken-3{color:#C62828 !important}.red.darken-4{background-color:#B71C1C !important}.red-text.text-darken-4{color:#B71C1C !important}.red.accent-1{background-color:#FF8A80 !important}.red-text.text-accent-1{color:#FF8A80 !important}.red.accent-2{background-color:#FF5252 !important}.red-text.text-accent-2{color:#FF5252 !important}.red.accent-3{background-color:#FF1744 !important}.red-text.text-accent-3{color:#FF1744 !important}.red.accent-4{background-color:#D50000 !important}.red-text.text-accent-4{color:#D50000 !important}.pink{background-color:#e91e63 !important}.pink-text{color:#e91e63 !important}.pink.lighten-5{background-color:#fce4ec !important}.pink-text.text-lighten-5{color:#fce4ec !important}.pink.lighten-4{background-color:#f8bbd0 !important}.pink-text.text-lighten-4{color:#f8bbd0 !important}.pink.lighten-3{background-color:#f48fb1 !important}.pink-text.text-lighten-3{color:#f48fb1 !important}.pink.lighten-2{background-color:#f06292 !important}.pink-text.text-lighten-2{color:#f06292 !important}.pink.lighten-1{background-color:#ec407a !important}.pink-text.text-lighten-1{color:#ec407a !important}.pink.darken-1{background-color:#d81b60 !important}.pink-text.text-darken-1{color:#d81b60 !important}.pink.darken-2{background-color:#c2185b !important}.pink-text.text-darken-2{color:#c2185b !important}.pink.darken-3{background-color:#ad1457 !important}.pink-text.text-darken-3{color:#ad1457 !important}.pink.darken-4{background-color:#880e4f !important}.pink-text.text-darken-4{color:#880e4f !important}.pink.accent-1{background-color:#ff80ab !important}.pink-text.text-accent-1{color:#ff80ab !important}.pink.accent-2{background-color:#ff4081 !important}.pink-text.text-accent-2{color:#ff4081 !important}.pink.accent-3{background-color:#f50057 !important}.pink-text.text-accent-3{color:#f50057 !important}.pink.accent-4{background-color:#c51162 !important}.pink-text.text-accent-4{color:#c51162 !important}.purple{background-color:#9c27b0 !important}.purple-text{color:#9c27b0 !important}.purple.lighten-5{background-color:#f3e5f5 !important}.purple-text.text-lighten-5{color:#f3e5f5 !important}.purple.lighten-4{background-color:#e1bee7 !important}.purple-text.text-lighten-4{color:#e1bee7 !important}.purple.lighten-3{background-color:#ce93d8 !important}.purple-text.text-lighten-3{color:#ce93d8 !important}.purple.lighten-2{background-color:#ba68c8 !important}.purple-text.text-lighten-2{color:#ba68c8 !important}.purple.lighten-1{background-color:#ab47bc !important}.purple-text.text-lighten-1{color:#ab47bc !important}.purple.darken-1{background-color:#8e24aa !important}.purple-text.text-darken-1{color:#8e24aa !important}.purple.darken-2{background-color:#7b1fa2 !important}.purple-text.text-darken-2{color:#7b1fa2 !important}.purple.darken-3{background-color:#6a1b9a !important}.purple-text.text-darken-3{color:#6a1b9a !important}.purple.darken-4{background-color:#4a148c !important}.purple-text.text-darken-4{color:#4a148c !important}.purple.accent-1{background-color:#ea80fc !important}.purple-text.text-accent-1{color:#ea80fc !important}.purple.accent-2{background-color:#e040fb !important}.purple-text.text-accent-2{color:#e040fb !important}.purple.accent-3{background-color:#d500f9 !important}.purple-text.text-accent-3{color:#d500f9 !important}.purple.accent-4{background-color:#a0f !important}.purple-text.text-accent-4{color:#a0f !important}.deep-purple{background-color:#673ab7 !important}.deep-purple-text{color:#673ab7 !important}.deep-purple.lighten-5{background-color:#ede7f6 !important}.deep-purple-text.text-lighten-5{color:#ede7f6 !important}.deep-purple.lighten-4{background-color:#d1c4e9 !important}.deep-purple-text.text-lighten-4{color:#d1c4e9 !important}.deep-purple.lighten-3{background-color:#b39ddb !important}.deep-purple-text.text-lighten-3{color:#b39ddb !important}.deep-purple.lighten-2{background-color:#9575cd !important}.deep-purple-text.text-lighten-2{color:#9575cd !important}.deep-purple.lighten-1{background-color:#7e57c2 !important}.deep-purple-text.text-lighten-1{color:#7e57c2 !important}.deep-purple.darken-1{background-color:#5e35b1 !important}.deep-purple-text.text-darken-1{color:#5e35b1 !important}.deep-purple.darken-2{background-color:#512da8 !important}.deep-purple-text.text-darken-2{color:#512da8 !important}.deep-purple.darken-3{background-color:#4527a0 !important}.deep-purple-text.text-darken-3{color:#4527a0 !important}.deep-purple.darken-4{background-color:#311b92 !important}.deep-purple-text.text-darken-4{color:#311b92 !important}.deep-purple.accent-1{background-color:#b388ff !important}.deep-purple-text.text-accent-1{color:#b388ff !important}.deep-purple.accent-2{background-color:#7c4dff !important}.deep-purple-text.text-accent-2{color:#7c4dff !important}.deep-purple.accent-3{background-color:#651fff !important}.deep-purple-text.text-accent-3{color:#651fff !important}.deep-purple.accent-4{background-color:#6200ea !important}.deep-purple-text.text-accent-4{color:#6200ea !important}.indigo{background-color:#3f51b5 !important}.indigo-text{color:#3f51b5 !important}.indigo.lighten-5{background-color:#e8eaf6 !important}.indigo-text.text-lighten-5{color:#e8eaf6 !important}.indigo.lighten-4{background-color:#c5cae9 !important}.indigo-text.text-lighten-4{color:#c5cae9 !important}.indigo.lighten-3{background-color:#9fa8da !important}.indigo-text.text-lighten-3{color:#9fa8da !important}.indigo.lighten-2{background-color:#7986cb !important}.indigo-text.text-lighten-2{color:#7986cb !important}.indigo.lighten-1{background-color:#5c6bc0 !important}.indigo-text.text-lighten-1{color:#5c6bc0 !important}.indigo.darken-1{background-color:#3949ab !important}.indigo-text.text-darken-1{color:#3949ab !important}.indigo.darken-2{background-color:#303f9f !important}.indigo-text.text-darken-2{color:#303f9f !important}.indigo.darken-3{background-color:#283593 !important}.indigo-text.text-darken-3{color:#283593 !important}.indigo.darken-4{background-color:#1a237e !important}.indigo-text.text-darken-4{color:#1a237e !important}.indigo.accent-1{background-color:#8c9eff !important}.indigo-text.text-accent-1{color:#8c9eff !important}.indigo.accent-2{background-color:#536dfe !important}.indigo-text.text-accent-2{color:#536dfe !important}.indigo.accent-3{background-color:#3d5afe !important}.indigo-text.text-accent-3{color:#3d5afe !important}.indigo.accent-4{background-color:#304ffe !important}.indigo-text.text-accent-4{color:#304ffe !important}.blue{background-color:#2196F3 !important}.blue-text{color:#2196F3 !important}.blue.lighten-5{background-color:#E3F2FD !important}.blue-text.text-lighten-5{color:#E3F2FD !important}.blue.lighten-4{background-color:#BBDEFB !important}.blue-text.text-lighten-4{color:#BBDEFB !important}.blue.lighten-3{background-color:#90CAF9 !important}.blue-text.text-lighten-3{color:#90CAF9 !important}.blue.lighten-2{background-color:#64B5F6 !important}.blue-text.text-lighten-2{color:#64B5F6 !important}.blue.lighten-1{background-color:#42A5F5 !important}.blue-text.text-lighten-1{color:#42A5F5 !important}.blue.darken-1{background-color:#1E88E5 !important}.blue-text.text-darken-1{color:#1E88E5 !important}.blue.darken-2{background-color:#1976D2 !important}.blue-text.text-darken-2{color:#1976D2 !important}.blue.darken-3{background-color:#1565C0 !important}.blue-text.text-darken-3{color:#1565C0 !important}.blue.darken-4{background-color:#0D47A1 !important}.blue-text.text-darken-4{color:#0D47A1 !important}.blue.accent-1{background-color:#82B1FF !important}.blue-text.text-accent-1{color:#82B1FF !important}.blue.accent-2{background-color:#448AFF !important}.blue-text.text-accent-2{color:#448AFF !important}.blue.accent-3{background-color:#2979FF !important}.blue-text.text-accent-3{color:#2979FF !important}.blue.accent-4{background-color:#2962FF !important}.blue-text.text-accent-4{color:#2962FF !important}.light-blue{background-color:#03a9f4 !important}.light-blue-text{color:#03a9f4 !important}.light-blue.lighten-5{background-color:#e1f5fe !important}.light-blue-text.text-lighten-5{color:#e1f5fe !important}.light-blue.lighten-4{background-color:#b3e5fc !important}.light-blue-text.text-lighten-4{color:#b3e5fc !important}.light-blue.lighten-3{background-color:#81d4fa !important}.light-blue-text.text-lighten-3{color:#81d4fa !important}.light-blue.lighten-2{background-color:#4fc3f7 !important}.light-blue-text.text-lighten-2{color:#4fc3f7 !important}.light-blue.lighten-1{background-color:#29b6f6 !important}.light-blue-text.text-lighten-1{color:#29b6f6 !important}.light-blue.darken-1{background-color:#039be5 !important}.light-blue-text.text-darken-1{color:#039be5 !important}.light-blue.darken-2{background-color:#0288d1 !important}.light-blue-text.text-darken-2{color:#0288d1 !important}.light-blue.darken-3{background-color:#0277bd !important}.light-blue-text.text-darken-3{color:#0277bd !important}.light-blue.darken-4{background-color:#01579b !important}.light-blue-text.text-darken-4{color:#01579b !important}.light-blue.accent-1{background-color:#80d8ff !important}.light-blue-text.text-accent-1{color:#80d8ff !important}.light-blue.accent-2{background-color:#40c4ff !important}.light-blue-text.text-accent-2{color:#40c4ff !important}.light-blue.accent-3{background-color:#00b0ff !important}.light-blue-text.text-accent-3{color:#00b0ff !important}.light-blue.accent-4{background-color:#0091ea !important}.light-blue-text.text-accent-4{color:#0091ea !important}.cyan{background-color:#00bcd4 !important}.cyan-text{color:#00bcd4 !important}.cyan.lighten-5{background-color:#e0f7fa !important}.cyan-text.text-lighten-5{color:#e0f7fa !important}.cyan.lighten-4{background-color:#b2ebf2 !important}.cyan-text.text-lighten-4{color:#b2ebf2 !important}.cyan.lighten-3{background-color:#80deea !important}.cyan-text.text-lighten-3{color:#80deea !important}.cyan.lighten-2{background-color:#4dd0e1 !important}.cyan-text.text-lighten-2{color:#4dd0e1 !important}.cyan.lighten-1{background-color:#26c6da !important}.cyan-text.text-lighten-1{color:#26c6da !important}.cyan.darken-1{background-color:#00acc1 !important}.cyan-text.text-darken-1{color:#00acc1 !important}.cyan.darken-2{background-color:#0097a7 !important}.cyan-text.text-darken-2{color:#0097a7 !important}.cyan.darken-3{background-color:#00838f !important}.cyan-text.text-darken-3{color:#00838f !important}.cyan.darken-4{background-color:#006064 !important}.cyan-text.text-darken-4{color:#006064 !important}.cyan.accent-1{background-color:#84ffff !important}.cyan-text.text-accent-1{color:#84ffff !important}.cyan.accent-2{background-color:#18ffff !important}.cyan-text.text-accent-2{color:#18ffff !important}.cyan.accent-3{background-color:#00e5ff !important}.cyan-text.text-accent-3{color:#00e5ff !important}.cyan.accent-4{background-color:#00b8d4 !important}.cyan-text.text-accent-4{color:#00b8d4 !important}.teal{background-color:#009688 !important}.teal-text{color:#009688 !important}.teal.lighten-5{background-color:#e0f2f1 !important}.teal-text.text-lighten-5{color:#e0f2f1 !important}.teal.lighten-4{background-color:#b2dfdb !important}.teal-text.text-lighten-4{color:#b2dfdb !important}.teal.lighten-3{background-color:#80cbc4 !important}.teal-text.text-lighten-3{color:#80cbc4 !important}.teal.lighten-2{background-color:#4db6ac !important}.teal-text.text-lighten-2{color:#4db6ac !important}.teal.lighten-1{background-color:#26a69a !important}.teal-text.text-lighten-1{color:#26a69a !important}.teal.darken-1{background-color:#00897b !important}.teal-text.text-darken-1{color:#00897b !important}.teal.darken-2{background-color:#00796b !important}.teal-text.text-darken-2{color:#00796b !important}.teal.darken-3{background-color:#00695c !important}.teal-text.text-darken-3{color:#00695c !important}.teal.darken-4{background-color:#004d40 !important}.teal-text.text-darken-4{color:#004d40 !important}.teal.accent-1{background-color:#a7ffeb !important}.teal-text.text-accent-1{color:#a7ffeb !important}.teal.accent-2{background-color:#64ffda !important}.teal-text.text-accent-2{color:#64ffda !important}.teal.accent-3{background-color:#1de9b6 !important}.teal-text.text-accent-3{color:#1de9b6 !important}.teal.accent-4{background-color:#00bfa5 !important}.teal-text.text-accent-4{color:#00bfa5 !important}.green{background-color:#4CAF50 !important}.green-text{color:#4CAF50 !important}.green.lighten-5{background-color:#E8F5E9 !important}.green-text.text-lighten-5{color:#E8F5E9 !important}.green.lighten-4{background-color:#C8E6C9 !important}.green-text.text-lighten-4{color:#C8E6C9 !important}.green.lighten-3{background-color:#A5D6A7 !important}.green-text.text-lighten-3{color:#A5D6A7 !important}.green.lighten-2{background-color:#81C784 !important}.green-text.text-lighten-2{color:#81C784 !important}.green.lighten-1{background-color:#66BB6A !important}.green-text.text-lighten-1{color:#66BB6A !important}.green.darken-1{background-color:#43A047 !important}.green-text.text-darken-1{color:#43A047 !important}.green.darken-2{background-color:#388E3C !important}.green-text.text-darken-2{color:#388E3C !important}.green.darken-3{background-color:#2E7D32 !important}.green-text.text-darken-3{color:#2E7D32 !important}.green.darken-4{background-color:#1B5E20 !important}.green-text.text-darken-4{color:#1B5E20 !important}.green.accent-1{background-color:#B9F6CA !important}.green-text.text-accent-1{color:#B9F6CA !important}.green.accent-2{background-color:#69F0AE !important}.green-text.text-accent-2{color:#69F0AE !important}.green.accent-3{background-color:#00E676 !important}.green-text.text-accent-3{color:#00E676 !important}.green.accent-4{background-color:#00C853 !important}.green-text.text-accent-4{color:#00C853 !important}.light-green{background-color:#8bc34a !important}.light-green-text{color:#8bc34a !important}.light-green.lighten-5{background-color:#f1f8e9 !important}.light-green-text.text-lighten-5{color:#f1f8e9 !important}.light-green.lighten-4{background-color:#dcedc8 !important}.light-green-text.text-lighten-4{color:#dcedc8 !important}.light-green.lighten-3{background-color:#c5e1a5 !important}.light-green-text.text-lighten-3{color:#c5e1a5 !important}.light-green.lighten-2{background-color:#aed581 !important}.light-green-text.text-lighten-2{color:#aed581 !important}.light-green.lighten-1{background-color:#9ccc65 !important}.light-green-text.text-lighten-1{color:#9ccc65 !important}.light-green.darken-1{background-color:#7cb342 !important}.light-green-text.text-darken-1{color:#7cb342 !important}.light-green.darken-2{background-color:#689f38 !important}.light-green-text.text-darken-2{color:#689f38 !important}.light-green.darken-3{background-color:#558b2f !important}.light-green-text.text-darken-3{color:#558b2f !important}.light-green.darken-4{background-color:#33691e !important}.light-green-text.text-darken-4{color:#33691e !important}.light-green.accent-1{background-color:#ccff90 !important}.light-green-text.text-accent-1{color:#ccff90 !important}.light-green.accent-2{background-color:#b2ff59 !important}.light-green-text.text-accent-2{color:#b2ff59 !important}.light-green.accent-3{background-color:#76ff03 !important}.light-green-text.text-accent-3{color:#76ff03 !important}.light-green.accent-4{background-color:#64dd17 !important}.light-green-text.text-accent-4{color:#64dd17 !important}.lime{background-color:#cddc39 !important}.lime-text{color:#cddc39 !important}.lime.lighten-5{background-color:#f9fbe7 !important}.lime-text.text-lighten-5{color:#f9fbe7 !important}.lime.lighten-4{background-color:#f0f4c3 !important}.lime-text.text-lighten-4{color:#f0f4c3 !important}.lime.lighten-3{background-color:#e6ee9c !important}.lime-text.text-lighten-3{color:#e6ee9c !important}.lime.lighten-2{background-color:#dce775 !important}.lime-text.text-lighten-2{color:#dce775 !important}.lime.lighten-1{background-color:#d4e157 !important}.lime-text.text-lighten-1{color:#d4e157 !important}.lime.darken-1{background-color:#c0ca33 !important}.lime-text.text-darken-1{color:#c0ca33 !important}.lime.darken-2{background-color:#afb42b !important}.lime-text.text-darken-2{color:#afb42b !important}.lime.darken-3{background-color:#9e9d24 !important}.lime-text.text-darken-3{color:#9e9d24 !important}.lime.darken-4{background-color:#827717 !important}.lime-text.text-darken-4{color:#827717 !important}.lime.accent-1{background-color:#f4ff81 !important}.lime-text.text-accent-1{color:#f4ff81 !important}.lime.accent-2{background-color:#eeff41 !important}.lime-text.text-accent-2{color:#eeff41 !important}.lime.accent-3{background-color:#c6ff00 !important}.lime-text.text-accent-3{color:#c6ff00 !important}.lime.accent-4{background-color:#aeea00 !important}.lime-text.text-accent-4{color:#aeea00 !important}.yellow{background-color:#ffeb3b !important}.yellow-text{color:#ffeb3b !important}.yellow.lighten-5{background-color:#fffde7 !important}.yellow-text.text-lighten-5{color:#fffde7 !important}.yellow.lighten-4{background-color:#fff9c4 !important}.yellow-text.text-lighten-4{color:#fff9c4 !important}.yellow.lighten-3{background-color:#fff59d !important}.yellow-text.text-lighten-3{color:#fff59d !important}.yellow.lighten-2{background-color:#fff176 !important}.yellow-text.text-lighten-2{color:#fff176 !important}.yellow.lighten-1{background-color:#ffee58 !important}.yellow-text.text-lighten-1{color:#ffee58 !important}.yellow.darken-1{background-color:#fdd835 !important}.yellow-text.text-darken-1{color:#fdd835 !important}.yellow.darken-2{background-color:#fbc02d !important}.yellow-text.text-darken-2{color:#fbc02d !important}.yellow.darken-3{background-color:#f9a825 !important}.yellow-text.text-darken-3{color:#f9a825 !important}.yellow.darken-4{background-color:#f57f17 !important}.yellow-text.text-darken-4{color:#f57f17 !important}.yellow.accent-1{background-color:#ffff8d !important}.yellow-text.text-accent-1{color:#ffff8d !important}.yellow.accent-2{background-color:#ff0 !important}.yellow-text.text-accent-2{color:#ff0 !important}.yellow.accent-3{background-color:#ffea00 !important}.yellow-text.text-accent-3{color:#ffea00 !important}.yellow.accent-4{background-color:#ffd600 !important}.yellow-text.text-accent-4{color:#ffd600 !important}.amber{background-color:#ffc107 !important}.amber-text{color:#ffc107 !important}.amber.lighten-5{background-color:#fff8e1 !important}.amber-text.text-lighten-5{color:#fff8e1 !important}.amber.lighten-4{background-color:#ffecb3 !important}.amber-text.text-lighten-4{color:#ffecb3 !important}.amber.lighten-3{background-color:#ffe082 !important}.amber-text.text-lighten-3{color:#ffe082 !important}.amber.lighten-2{background-color:#ffd54f !important}.amber-text.text-lighten-2{color:#ffd54f !important}.amber.lighten-1{background-color:#ffca28 !important}.amber-text.text-lighten-1{color:#ffca28 !important}.amber.darken-1{background-color:#ffb300 !important}.amber-text.text-darken-1{color:#ffb300 !important}.amber.darken-2{background-color:#ffa000 !important}.amber-text.text-darken-2{color:#ffa000 !important}.amber.darken-3{background-color:#ff8f00 !important}.amber-text.text-darken-3{color:#ff8f00 !important}.amber.darken-4{background-color:#ff6f00 !important}.amber-text.text-darken-4{color:#ff6f00 !important}.amber.accent-1{background-color:#ffe57f !important}.amber-text.text-accent-1{color:#ffe57f !important}.amber.accent-2{background-color:#ffd740 !important}.amber-text.text-accent-2{color:#ffd740 !important}.amber.accent-3{background-color:#ffc400 !important}.amber-text.text-accent-3{color:#ffc400 !important}.amber.accent-4{background-color:#ffab00 !important}.amber-text.text-accent-4{color:#ffab00 !important}.orange{background-color:#ff9800 !important}.orange-text{color:#ff9800 !important}.orange.lighten-5{background-color:#fff3e0 !important}.orange-text.text-lighten-5{color:#fff3e0 !important}.orange.lighten-4{background-color:#ffe0b2 !important}.orange-text.text-lighten-4{color:#ffe0b2 !important}.orange.lighten-3{background-color:#ffcc80 !important}.orange-text.text-lighten-3{color:#ffcc80 !important}.orange.lighten-2{background-color:#ffb74d !important}.orange-text.text-lighten-2{color:#ffb74d !important}.orange.lighten-1{background-color:#ffa726 !important}.orange-text.text-lighten-1{color:#ffa726 !important}.orange.darken-1{background-color:#fb8c00 !important}.orange-text.text-darken-1{color:#fb8c00 !important}.orange.darken-2{background-color:#f57c00 !important}.orange-text.text-darken-2{color:#f57c00 !important}.orange.darken-3{background-color:#ef6c00 !important}.orange-text.text-darken-3{color:#ef6c00 !important}.orange.darken-4{background-color:#e65100 !important}.orange-text.text-darken-4{color:#e65100 !important}.orange.accent-1{background-color:#ffd180 !important}.orange-text.text-accent-1{color:#ffd180 !important}.orange.accent-2{background-color:#ffab40 !important}.orange-text.text-accent-2{color:#ffab40 !important}.orange.accent-3{background-color:#ff9100 !important}.orange-text.text-accent-3{color:#ff9100 !important}.orange.accent-4{background-color:#ff6d00 !important}.orange-text.text-accent-4{color:#ff6d00 !important}.deep-orange{background-color:#ff5722 !important}.deep-orange-text{color:#ff5722 !important}.deep-orange.lighten-5{background-color:#fbe9e7 !important}.deep-orange-text.text-lighten-5{color:#fbe9e7 !important}.deep-orange.lighten-4{background-color:#ffccbc !important}.deep-orange-text.text-lighten-4{color:#ffccbc !important}.deep-orange.lighten-3{background-color:#ffab91 !important}.deep-orange-text.text-lighten-3{color:#ffab91 !important}.deep-orange.lighten-2{background-color:#ff8a65 !important}.deep-orange-text.text-lighten-2{color:#ff8a65 !important}.deep-orange.lighten-1{background-color:#ff7043 !important}.deep-orange-text.text-lighten-1{color:#ff7043 !important}.deep-orange.darken-1{background-color:#f4511e !important}.deep-orange-text.text-darken-1{color:#f4511e !important}.deep-orange.darken-2{background-color:#e64a19 !important}.deep-orange-text.text-darken-2{color:#e64a19 !important}.deep-orange.darken-3{background-color:#d84315 !important}.deep-orange-text.text-darken-3{color:#d84315 !important}.deep-orange.darken-4{background-color:#bf360c !important}.deep-orange-text.text-darken-4{color:#bf360c !important}.deep-orange.accent-1{background-color:#ff9e80 !important}.deep-orange-text.text-accent-1{color:#ff9e80 !important}.deep-orange.accent-2{background-color:#ff6e40 !important}.deep-orange-text.text-accent-2{color:#ff6e40 !important}.deep-orange.accent-3{background-color:#ff3d00 !important}.deep-orange-text.text-accent-3{color:#ff3d00 !important}.deep-orange.accent-4{background-color:#dd2c00 !important}.deep-orange-text.text-accent-4{color:#dd2c00 !important}.brown{background-color:#795548 !important}.brown-text{color:#795548 !important}.brown.lighten-5{background-color:#efebe9 !important}.brown-text.text-lighten-5{color:#efebe9 !important}.brown.lighten-4{background-color:#d7ccc8 !important}.brown-text.text-lighten-4{color:#d7ccc8 !important}.brown.lighten-3{background-color:#bcaaa4 !important}.brown-text.text-lighten-3{color:#bcaaa4 !important}.brown.lighten-2{background-color:#a1887f !important}.brown-text.text-lighten-2{color:#a1887f !important}.brown.lighten-1{background-color:#8d6e63 !important}.brown-text.text-lighten-1{color:#8d6e63 !important}.brown.darken-1{background-color:#6d4c41 !important}.brown-text.text-darken-1{color:#6d4c41 !important}.brown.darken-2{background-color:#5d4037 !important}.brown-text.text-darken-2{color:#5d4037 !important}.brown.darken-3{background-color:#4e342e !important}.brown-text.text-darken-3{color:#4e342e !important}.brown.darken-4{background-color:#3e2723 !important}.brown-text.text-darken-4{color:#3e2723 !important}.blue-grey{background-color:#607d8b !important}.blue-grey-text{color:#607d8b !important}.blue-grey.lighten-5{background-color:#eceff1 !important}.blue-grey-text.text-lighten-5{color:#eceff1 !important}.blue-grey.lighten-4{background-color:#cfd8dc !important}.blue-grey-text.text-lighten-4{color:#cfd8dc !important}.blue-grey.lighten-3{background-color:#b0bec5 !important}.blue-grey-text.text-lighten-3{color:#b0bec5 !important}.blue-grey.lighten-2{background-color:#90a4ae !important}.blue-grey-text.text-lighten-2{color:#90a4ae !important}.blue-grey.lighten-1{background-color:#78909c !important}.blue-grey-text.text-lighten-1{color:#78909c !important}.blue-grey.darken-1{background-color:#546e7a !important}.blue-grey-text.text-darken-1{color:#546e7a !important}.blue-grey.darken-2{background-color:#455a64 !important}.blue-grey-text.text-darken-2{color:#455a64 !important}.blue-grey.darken-3{background-color:#37474f !important}.blue-grey-text.text-darken-3{color:#37474f !important}.blue-grey.darken-4{background-color:#263238 !important}.blue-grey-text.text-darken-4{color:#263238 !important}.grey{background-color:#9e9e9e !important}.grey-text{color:#9e9e9e !important}.grey.lighten-5{background-color:#fafafa !important}.grey-text.text-lighten-5{color:#fafafa !important}.grey.lighten-4{background-color:#f5f5f5 !important}.grey-text.text-lighten-4{color:#f5f5f5 !important}.grey.lighten-3{background-color:#eee !important}.grey-text.text-lighten-3{color:#eee !important}.grey.lighten-2{background-color:#e0e0e0 !important}.grey-text.text-lighten-2{color:#e0e0e0 !important}.grey.lighten-1{background-color:#bdbdbd !important}.grey-text.text-lighten-1{color:#bdbdbd !important}.grey.darken-1{background-color:#757575 !important}.grey-text.text-darken-1{color:#757575 !important}.grey.darken-2{background-color:#616161 !important}.grey-text.text-darken-2{color:#616161 !important}.grey.darken-3{background-color:#424242 !important}.grey-text.text-darken-3{color:#424242 !important}.grey.darken-4{background-color:#212121 !important}.grey-text.text-darken-4{color:#212121 !important}.shades.black{background-color:#000 !important}.shades-text.text-black{color:#000 !important}.shades.white{background-color:#fff !important}.shades-text.text-white{color:#fff !important}.shades.transparent{background-color:transparent !important}.shades-text.text-transparent{color:transparent !important}.black{background-color:#000 !important}.black-text{color:#000 !important}.white{background-color:#fff !important}.white-text{color:#fff !important}.transparent{background-color:transparent !important}.transparent-text{color:transparent !important}/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}ul{list-style-type:none}ul.browser-default{list-style-type:initial}a{color:#039be5;text-decoration:none;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:flex;align-items:center}.valign-wrapper .valign{display:block}ul{padding:0}ul li{list-style-type:none}.clearfix{clear:both}.z-depth-0{box-shadow:none !important}.z-depth-1,nav,.card-panel,.card,.toast,.btn,.btn-large,.btn-floating,.dropdown-content,.collapsible,.side-nav{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}.z-depth-1-half,.btn:hover,.btn-large:hover,.btn-floating:hover{box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15)}.z-depth-2{box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}.z-depth-3{box-shadow:0 12px 15px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19)}.z-depth-4,.modal{box-shadow:0 16px 28px 0 rgba(0,0,0,0.22),0 25px 55px 0 rgba(0,0,0,0.21)}.z-depth-5{box-shadow:0 27px 24px 0 rgba(0,0,0,0.2),0 40px 77px 0 rgba(0,0,0,0.22)}.hoverable{transition:box-shadow .25s;box-shadow:0}.hoverable:hover{transition:box-shadow .25s;box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #ee6e73}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px;border-radius:2px;text-align:center}.pagination li a{color:#444}.pagination li.active a{color:#fff}.pagination li.active{background-color:#ee6e73}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2.2rem;vertical-align:middle}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width: 992px){.pagination{width:100%}.pagination li.prev,.pagination li.next{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:rgba(255,255,255,0.7)}.breadcrumb i,.breadcrumb [class^="mdi-"],.breadcrumb [class*="mdi-"],.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:'\E5CC';color:rgba(255,255,255,0.7);vertical-align:top;display:inline-block;font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}.parallax img{display:none;position:absolute;left:50%;bottom:0;min-width:100%;min-height:100%;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);transform:translateX(-50%)}.pin-top,.pin-bottom{position:relative}.pinned{position:fixed !important}ul.staggered-list li{opacity:0}.fade-in{opacity:0;transform-origin:0 50%}@media only screen and (max-width: 600px){.hide-on-small-only,.hide-on-small-and-down{display:none !important}}@media only screen and (max-width: 992px){.hide-on-med-and-down{display:none !important}}@media only screen and (min-width: 601px){.hide-on-med-and-up{display:none !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.hide-on-med-only{display:none !important}}@media only screen and (min-width: 993px){.hide-on-large-only{display:none !important}}@media only screen and (min-width: 993px){.show-on-large{display:block !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.show-on-medium{display:block !important}}@media only screen and (max-width: 600px){.show-on-small{display:block !important}}@media only screen and (min-width: 601px){.show-on-medium-and-up{display:block !important}}@media only screen and (max-width: 992px){.show-on-medium-and-down{display:block !important}}@media only screen and (max-width: 600px){.center-on-small-only{text-align:center}}footer.page-footer{margin-top:20px;padding-top:20px;background-color:#ee6e73}footer.page-footer .footer-copyright{overflow:hidden;height:50px;line-height:50px;color:rgba(255,255,255,0.8);background-color:rgba(51,51,51,0.08)}table,th,td{border:none}table{width:100%;display:table}table.bordered>thead>tr,table.bordered>tbody>tr{border-bottom:1px solid #d0d0d0}table.striped>tbody>tr:nth-child(odd){background-color:#f2f2f2}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:#f2f2f2}table.centered thead tr th,table.centered tbody tr td{text-align:center}thead{border-bottom:1px solid #d0d0d0}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width: 992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:'\00a0'}table.responsive-table th,table.responsive-table td{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th::before{content:"\00a0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid #d0d0d0}table.responsive-table.bordered th{border-bottom:0;border-left:0}table.responsive-table.bordered td{border-left:0;border-right:0;border-bottom:0}table.responsive-table.bordered tr{border:0}table.responsive-table.bordered tbody tr{border-right:1px solid #d0d0d0}}.collection{margin:.5rem 0 1rem 0;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar .circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#26a69a;color:#eafaf9}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;transition:.25s;color:#26a69a}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#26a69a}.collapsible .collection{margin:0;border:none}span.badge{min-width:3rem;padding:0 6px;text-align:center;font-size:1rem;line-height:inherit;color:#757575;position:absolute;right:15px;box-sizing:border-box}span.badge.new{font-weight:300;font-size:0.8rem;color:#fff;background-color:#26a69a;border-radius:2px}span.badge.new:after{content:" new"}nav ul a span.badge{position:static;margin-left:4px;line-height:0}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container iframe,.video-container object,.video-container embed{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#acece6;border-radius:2px;margin:.5rem 0 1rem 0;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;background-color:#26a69a;transition:width .3s linear}.progress .indeterminate{background-color:#26a69a}.progress .indeterminate:before{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite}.progress .indeterminate:after{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;animation-delay:1.15s}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}.hide{display:none !important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left !important}.right{float:right !important}.no-select,input[type=range],input[type=range]+.thumb{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0 !important}.material-icons{text-rendering:optimizeLegibility;font-feature-settings:'liga'}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width: 601px){.container{width:85%}}@media only screen and (min-width: 993px){.container{width:70%}}.container .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;box-sizing:border-box;padding:0 .75rem}.row .col[class*="push-"],.row .col[class*="pull-"]{position:relative}.row .col.s1{width:8.33333%;margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.66667%;margin-left:auto;left:auto;right:auto}.row .col.s3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.33333%;margin-left:auto;left:auto;right:auto}.row .col.s5{width:41.66667%;margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.s7{width:58.33333%;margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.66667%;margin-left:auto;left:auto;right:auto}.row .col.s9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.33333%;margin-left:auto;left:auto;right:auto}.row .col.s11{width:91.66667%;margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-s1{margin-left:8.33333%}.row .col.pull-s1{right:8.33333%}.row .col.push-s1{left:8.33333%}.row .col.offset-s2{margin-left:16.66667%}.row .col.pull-s2{right:16.66667%}.row .col.push-s2{left:16.66667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.33333%}.row .col.pull-s4{right:33.33333%}.row .col.push-s4{left:33.33333%}.row .col.offset-s5{margin-left:41.66667%}.row .col.pull-s5{right:41.66667%}.row .col.push-s5{left:41.66667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.33333%}.row .col.pull-s7{right:58.33333%}.row .col.push-s7{left:58.33333%}.row .col.offset-s8{margin-left:66.66667%}.row .col.pull-s8{right:66.66667%}.row .col.push-s8{left:66.66667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.33333%}.row .col.pull-s10{right:83.33333%}.row .col.push-s10{left:83.33333%}.row .col.offset-s11{margin-left:91.66667%}.row .col.pull-s11{right:91.66667%}.row .col.push-s11{left:91.66667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width: 601px){.row .col.m1{width:8.33333%;margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.66667%;margin-left:auto;left:auto;right:auto}.row .col.m3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.33333%;margin-left:auto;left:auto;right:auto}.row .col.m5{width:41.66667%;margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.m7{width:58.33333%;margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.66667%;margin-left:auto;left:auto;right:auto}.row .col.m9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.33333%;margin-left:auto;left:auto;right:auto}.row .col.m11{width:91.66667%;margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-m1{margin-left:8.33333%}.row .col.pull-m1{right:8.33333%}.row .col.push-m1{left:8.33333%}.row .col.offset-m2{margin-left:16.66667%}.row .col.pull-m2{right:16.66667%}.row .col.push-m2{left:16.66667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.33333%}.row .col.pull-m4{right:33.33333%}.row .col.push-m4{left:33.33333%}.row .col.offset-m5{margin-left:41.66667%}.row .col.pull-m5{right:41.66667%}.row .col.push-m5{left:41.66667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.33333%}.row .col.pull-m7{right:58.33333%}.row .col.push-m7{left:58.33333%}.row .col.offset-m8{margin-left:66.66667%}.row .col.pull-m8{right:66.66667%}.row .col.push-m8{left:66.66667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.33333%}.row .col.pull-m10{right:83.33333%}.row .col.push-m10{left:83.33333%}.row .col.offset-m11{margin-left:91.66667%}.row .col.pull-m11{right:91.66667%}.row .col.push-m11{left:91.66667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width: 993px){.row .col.l1{width:8.33333%;margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.66667%;margin-left:auto;left:auto;right:auto}.row .col.l3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.33333%;margin-left:auto;left:auto;right:auto}.row .col.l5{width:41.66667%;margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.l7{width:58.33333%;margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.66667%;margin-left:auto;left:auto;right:auto}.row .col.l9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.33333%;margin-left:auto;left:auto;right:auto}.row .col.l11{width:91.66667%;margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-l1{margin-left:8.33333%}.row .col.pull-l1{right:8.33333%}.row .col.push-l1{left:8.33333%}.row .col.offset-l2{margin-left:16.66667%}.row .col.pull-l2{right:16.66667%}.row .col.push-l2{left:16.66667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.33333%}.row .col.pull-l4{right:33.33333%}.row .col.push-l4{left:33.33333%}.row .col.offset-l5{margin-left:41.66667%}.row .col.pull-l5{right:41.66667%}.row .col.push-l5{left:41.66667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.33333%}.row .col.pull-l7{right:58.33333%}.row .col.push-l7{left:58.33333%}.row .col.offset-l8{margin-left:66.66667%}.row .col.pull-l8{right:66.66667%}.row .col.push-l8{left:66.66667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.33333%}.row .col.pull-l10{right:83.33333%}.row .col.push-l10{left:83.33333%}.row .col.offset-l11{margin-left:91.66667%}.row .col.pull-l11{right:91.66667%}.row .col.push-l11{left:91.66667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}nav{color:#fff;background-color:#ee6e73;width:100%;height:56px;line-height:56px}nav a{color:#fff}nav i,nav [class^="mdi-"],nav [class*="mdi-"],nav i.material-icons{display:block;font-size:2rem;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width: 993px){nav a.button-collapse{display:none}}nav .button-collapse{float:left;position:relative;z-index:1;height:56px}nav .button-collapse i{font-size:2.7rem;height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0;white-space:nowrap}nav .brand-logo.center{left:50%;transform:translateX(-50%)}@media only screen and (max-width: 992px){nav .brand-logo{left:50%;transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;transform:none}nav .brand-logo.left{left:0.5rem}nav .brand-logo.right{right:0.5rem;left:auto}}nav .brand-logo.right{right:0.5rem;padding:0}nav ul{margin:0}nav ul li{transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,0.1)}nav ul a{transition:background-color .3s;font-size:1rem;color:#fff;display:inline-block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-large,nav ul a.btn-large,nav ul a.btn-flat,nav ul a.btn-floating{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a:hover{background-color:rgba(0,0,0,0.1)}nav ul.left{float:left}nav .input-field{margin:0}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=text]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=url]:valid,nav .input-field input[type=date]:valid{border:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:rgba(255,255,255,0.7);transition:color .3s}nav .input-field label.active i{color:#fff}nav .input-field label.active{transform:translateY(0)}.navbar-fixed{position:relative;height:56px;z-index:998}.navbar-fixed nav{position:fixed}@media only screen and (min-width: 601px){nav,nav .nav-wrapper i,nav a.button-collapse,nav a.button-collapse i{height:64px;line-height:64px}.navbar-fixed{height:64px}}@font-face{font-family:"Roboto";src:local(Roboto Thin),url("../fonts/roboto/Roboto-Thin.eot");src:url("../fonts/roboto/Roboto-Thin.eot?#iefix") format("embedded-opentype"),url("../fonts/roboto/Roboto-Thin.woff2") format("woff2"),url("../fonts/roboto/Roboto-Thin.woff") format("woff"),url("../fonts/roboto/Roboto-Thin.ttf") format("truetype");font-weight:200}@font-face{font-family:"Roboto";src:local(Roboto Light),url("../fonts/roboto/Roboto-Light.eot");src:url("../fonts/roboto/Roboto-Light.eot?#iefix") format("embedded-opentype"),url("../fonts/roboto/Roboto-Light.woff2") format("woff2"),url("../fonts/roboto/Roboto-Light.woff") format("woff"),url("../fonts/roboto/Roboto-Light.ttf") format("truetype");font-weight:300}@font-face{font-family:"Roboto";src:local(Roboto Regular),url("../fonts/roboto/Roboto-Regular.eot");src:url("../fonts/roboto/Roboto-Regular.eot?#iefix") format("embedded-opentype"),url("../fonts/roboto/Roboto-Regular.woff2") format("woff2"),url("../fonts/roboto/Roboto-Regular.woff") format("woff"),url("../fonts/roboto/Roboto-Regular.ttf") format("truetype");font-weight:400}@font-face{font-family:"Roboto";src:url("../fonts/roboto/Roboto-Medium.eot");src:url("../fonts/roboto/Roboto-Medium.eot?#iefix") format("embedded-opentype"),url("../fonts/roboto/Roboto-Medium.woff2") format("woff2"),url("../fonts/roboto/Roboto-Medium.woff") format("woff"),url("../fonts/roboto/Roboto-Medium.ttf") format("truetype");font-weight:500}@font-face{font-family:"Roboto";src:url("../fonts/roboto/Roboto-Bold.eot");src:url("../fonts/roboto/Roboto-Bold.eot?#iefix") format("embedded-opentype"),url("../fonts/roboto/Roboto-Bold.woff2") format("woff2"),url("../fonts/roboto/Roboto-Bold.woff") format("woff"),url("../fonts/roboto/Roboto-Bold.ttf") format("truetype");font-weight:700}a{text-decoration:none}html{line-height:1.5;font-family:"Roboto", sans-serif;font-weight:normal;color:rgba(0,0,0,0.87)}@media only screen and (min-width: 0){html{font-size:14px}}@media only screen and (min-width: 992px){html{font-size:14.5px}}@media only screen and (min-width: 1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.1}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;line-height:110%;margin:2.1rem 0 1.68rem 0}h2{font-size:3.56rem;line-height:110%;margin:1.78rem 0 1.424rem 0}h3{font-size:2.92rem;line-height:110%;margin:1.46rem 0 1.168rem 0}h4{font-size:2.28rem;line-height:110%;margin:1.14rem 0 .912rem 0}h5{font-size:1.64rem;line-height:110%;margin:.82rem 0 .656rem 0}h6{font-size:1rem;line-height:110%;margin:.5rem 0 .4rem 0}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light,footer.page-footer .footer-copyright{font-weight:300}.thin{font-weight:200}.flow-text{font-weight:300}@media only screen and (min-width: 360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width: 390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width: 420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width: 450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width: 480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width: 510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width: 540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width: 570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width: 600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width: 630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width: 660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width: 690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width: 720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width: 750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width: 780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width: 810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width: 840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width: 870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width: 900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width: 930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width: 960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width: 360px){.flow-text{font-size:1.2rem}}.card-panel{transition:box-shadow .25s;padding:20px;margin:.5rem 0 1rem 0;border-radius:2px;background-color:#fff}.card{position:relative;margin:.5rem 0 1rem 0;background-color:#fff;transition:box-shadow .25s;border-radius:2px}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.small,.card.medium,.card.large{position:relative}.card.small .card-image,.card.medium .card-image,.card.large .card-image{max-height:60%;overflow:hidden}.card.small .card-content,.card.medium .card-content,.card.large .card-content{max-height:40%;overflow:hidden}.card.small .card-action,.card.medium .card-action,.card.large .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#fff;position:absolute;bottom:0;left:0;padding:20px}.card .card-content{padding:20px;border-radius:0 0 2px 2px}.card .card-content p{margin:0;color:inherit}.card .card-content .card-title{line-height:48px}.card .card-action{position:relative;background-color:inherit;border-top:1px solid rgba(160,160,160,0.2);padding:20px;z-index:2}.card .card-action a:not(.btn):not(.btn-large):not(.btn-large):not(.btn-floating){color:#ffab40;margin-right:20px;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-large):not(.btn-large):not(.btn-floating):hover{color:#ffd8a6}.card .card-action+.card-reveal{z-index:1;padding-bottom:64px}.card .card-reveal{padding:20px;position:absolute;background-color:#fff;width:100%;overflow-y:auto;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width: 600px){#toast-container{min-width:100%;bottom:0%}}@media only screen and (min-width: 601px) and (max-width: 992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width: 993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:0;width:auto;clear:both;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;word-break:break-all;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:flex;align-items:center;justify-content:space-between}.toast .btn,.toast .btn-large,.toast .btn-flat{margin:0;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width: 600px){.toast{width:100%;border-radius:0}}@media only screen and (min-width: 601px) and (max-width: 992px){.toast{float:left}}@media only screen and (min-width: 993px){.toast{float:right}}.tabs{display:flex;position:relative;overflow-x:auto;overflow-y:hidden;height:48px;background-color:#fff;margin:0 auto;width:100%;white-space:nowrap}.tabs .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;display:block;float:left;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase;text-overflow:ellipsis;overflow:hidden;letter-spacing:.8px;width:15%;min-width:80px}.tabs .tab a{color:#ee6e73;display:block;width:100%;height:100%;text-overflow:ellipsis;overflow:hidden;transition:color .28s ease}.tabs .tab a:hover{color:#f9c9cb}.tabs .tab.disabled a{color:#f9c9cb;cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#f6b2b5;will-change:left, right}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;opacity:0;display:none;position:absolute;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none;will-change:top, left}.backdrop{position:absolute;opacity:0;display:none;height:7px;width:14px;border-radius:0 0 14px 14px;background-color:#323232;z-index:-1;transform-origin:50% 10%;will-change:transform, opacity}.btn,.btn-large,.btn-flat{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;outline:0;padding:0 2rem;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn.disabled,.disabled.btn-large,.btn-floating.disabled,.btn-large.disabled,.btn:disabled .btn-large:disabled,.btn-large:disabled .btn-large:disabled,.btn-floating:disabled{background-color:#DFDFDF !important;box-shadow:none;color:#9F9F9F !important;cursor:default}.btn.disabled *,.disabled.btn-large *,.btn-floating.disabled *,.btn-large.disabled *,.btn:disabled .btn-large:disabled *,.btn-large:disabled .btn-large:disabled *,.btn-floating:disabled *{pointer-events:none}.btn.disabled:hover,.disabled.btn-large:hover,.btn-floating.disabled:hover,.btn-large.disabled:hover,.btn:disabled .btn-large:disabled:hover,.btn-large:disabled .btn-large:disabled:hover,.btn-floating:disabled:hover{background-color:#DFDFDF !important;color:#9F9F9F !important}.btn i,.btn-large i,.btn-floating i,.btn-large i,.btn-flat i{font-size:1.3rem;line-height:inherit}.btn,.btn-large{text-decoration:none;color:#fff;background-color:#26a69a;text-align:center;letter-spacing:.5px;transition:.2s ease-out;cursor:pointer}.btn:hover,.btn-large:hover{background-color:#2bbbad}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:37px;height:37px;line-height:37px;padding:0;background-color:#26a69a;border-radius:50%;transition:.3s;cursor:pointer;vertical-align:middle}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:37px}.btn-floating:hover{background-color:#26a69a}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:55.5px;height:55.5px}.btn-floating.btn-large i{line-height:55.5px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:998}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.horizontal{padding:0 0 0 15px}.fixed-action-btn.horizontal ul{text-align:right;right:64px;top:50%;transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.horizontal ul li{display:inline-block;margin:15px 15px 0 0}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.btn-flat{box-shadow:none;background-color:transparent;color:#343434;cursor:pointer}.btn-flat.disabled{color:#b3b3b3;cursor:default}.btn-large{height:54px;line-height:54px}.btn-large i{font-size:1.6rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;max-height:650px;overflow-y:auto;opacity:0;position:absolute;z-index:999;will-change:width, height}.dropdown-content li{clear:both;color:rgba(0,0,0,0.87);cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left;text-transform:none}.dropdown-content li:hover,.dropdown-content li.active,.dropdown-content li.selected{background-color:#eee}.dropdown-content li.active.selected{background-color:#e1e1e1}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#26a69a;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:3px;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit}/*!
+ * Waves v0.6.0
+ * http://fian.my.id/Waves
+ *
+ * Copyright 2014 Alfiana E. Sibuea and other contributors
+ * Released under the MIT license
+ * https://github.com/fians/Waves/blob/master/LICENSE
+ */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;will-change:opacity, transform;transition:all .3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);transition:all 0.7s ease-out;transition-property:transform, opacity;transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-notransition{transition:none !important}.waves-circle{transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, #fff 100%, #000 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}a.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}@media only screen and (max-width: 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-flat{float:right;margin:6px 0}.lean-overlay{position:fixed;z-index:999;top:-100px;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:block;cursor:pointer;min-height:3rem;line-height:3rem;padding:0 1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header i{width:2rem;font-size:1.6rem;line-height:3rem;display:block;float:left;text-align:center;margin-right:1rem}.collapsible-body{display:none;border-bottom:1px solid #ddd;box-sizing:border-box}.collapsible-body p{margin:0;padding:2rem}.side-nav .collapsible,.side-nav.fixed .collapsible{border:none;box-shadow:none}.side-nav .collapsible li,.side-nav.fixed .collapsible li{padding:0}.side-nav .collapsible-header,.side-nav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 30px}.side-nav .collapsible-header:hover,.side-nav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.side-nav .collapsible-header i,.side-nav.fixed .collapsible-header i{line-height:inherit}.side-nav .collapsible-body,.side-nav.fixed .collapsible-body{border:0;background-color:#fff}.side-nav .collapsible-body li a,.side-nav.fixed .collapsible-body li a{padding:0 37.5px 0 45px}.collapsible.popout{border:none;box-shadow:none}.collapsible.popout>li{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4}.chip img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip i.material-icons{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.materialboxed{display:block;cursor:zoom-in;position:relative;transition:opacity .4s}.materialboxed:hover{will-change:left, top, width, height}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#9e9e9e}::-webkit-input-placeholder{color:#d1d1d1}:-moz-placeholder{color:#d1d1d1}::-moz-placeholder{color:#d1d1d1}:-ms-input-placeholder{color:#d1d1d1}input:not([type]),input[type=text],input[type=password],input[type=email],input[type=url],input[type=time],input[type=date],input[type=datetime],input[type=datetime-local],input[type=tel],input[type=number],input[type=search],textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:1rem;margin:0 0 15px 0;padding:0;box-shadow:none;box-sizing:content-box;transition:all 0.3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:disabled,input[type=text][readonly="readonly"],input[type=password]:disabled,input[type=password][readonly="readonly"],input[type=email]:disabled,input[type=email][readonly="readonly"],input[type=url]:disabled,input[type=url][readonly="readonly"],input[type=time]:disabled,input[type=time][readonly="readonly"],input[type=date]:disabled,input[type=date][readonly="readonly"],input[type=datetime]:disabled,input[type=datetime][readonly="readonly"],input[type=datetime-local]:disabled,input[type=datetime-local][readonly="readonly"],input[type=tel]:disabled,input[type=tel][readonly="readonly"],input[type=number]:disabled,input[type=number][readonly="readonly"],input[type=search]:disabled,input[type=search][readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.26);border-bottom:1px dotted rgba(0,0,0,0.26)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:disabled+label,input[type=text][readonly="readonly"]+label,input[type=password]:disabled+label,input[type=password][readonly="readonly"]+label,input[type=email]:disabled+label,input[type=email][readonly="readonly"]+label,input[type=url]:disabled+label,input[type=url][readonly="readonly"]+label,input[type=time]:disabled+label,input[type=time][readonly="readonly"]+label,input[type=date]:disabled+label,input[type=date][readonly="readonly"]+label,input[type=datetime]:disabled+label,input[type=datetime][readonly="readonly"]+label,input[type=datetime-local]:disabled+label,input[type=datetime-local][readonly="readonly"]+label,input[type=tel]:disabled+label,input[type=tel][readonly="readonly"]+label,input[type=number]:disabled+label,input[type=number][readonly="readonly"]+label,input[type=search]:disabled+label,input[type=search][readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.26)}input:not([type]):focus:not([readonly]),input[type=text]:focus:not([readonly]),input[type=password]:focus:not([readonly]),input[type=email]:focus:not([readonly]),input[type=url]:focus:not([readonly]),input[type=time]:focus:not([readonly]),input[type=date]:focus:not([readonly]),input[type=datetime]:focus:not([readonly]),input[type=datetime-local]:focus:not([readonly]),input[type=tel]:focus:not([readonly]),input[type=number]:focus:not([readonly]),input[type=search]:focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=text]:focus:not([readonly])+label,input[type=password]:focus:not([readonly])+label,input[type=email]:focus:not([readonly])+label,input[type=url]:focus:not([readonly])+label,input[type=time]:focus:not([readonly])+label,input[type=date]:focus:not([readonly])+label,input[type=datetime]:focus:not([readonly])+label,input[type=datetime-local]:focus:not([readonly])+label,input[type=tel]:focus:not([readonly])+label,input[type=number]:focus:not([readonly])+label,input[type=search]:focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]).valid,input:not([type]):focus.valid,input[type=text].valid,input[type=text]:focus.valid,input[type=password].valid,input[type=password]:focus.valid,input[type=email].valid,input[type=email]:focus.valid,input[type=url].valid,input[type=url]:focus.valid,input[type=time].valid,input[type=time]:focus.valid,input[type=date].valid,input[type=date]:focus.valid,input[type=datetime].valid,input[type=datetime]:focus.valid,input[type=datetime-local].valid,input[type=datetime-local]:focus.valid,input[type=tel].valid,input[type=tel]:focus.valid,input[type=number].valid,input[type=number]:focus.valid,input[type=search].valid,input[type=search]:focus.valid,textarea.materialize-textarea.valid,textarea.materialize-textarea:focus.valid{border-bottom:1px solid #4CAF50;box-shadow:0 1px 0 0 #4CAF50}input:not([type]).valid+label:after,input:not([type]):focus.valid+label:after,input[type=text].valid+label:after,input[type=text]:focus.valid+label:after,input[type=password].valid+label:after,input[type=password]:focus.valid+label:after,input[type=email].valid+label:after,input[type=email]:focus.valid+label:after,input[type=url].valid+label:after,input[type=url]:focus.valid+label:after,input[type=time].valid+label:after,input[type=time]:focus.valid+label:after,input[type=date].valid+label:after,input[type=date]:focus.valid+label:after,input[type=datetime].valid+label:after,input[type=datetime]:focus.valid+label:after,input[type=datetime-local].valid+label:after,input[type=datetime-local]:focus.valid+label:after,input[type=tel].valid+label:after,input[type=tel]:focus.valid+label:after,input[type=number].valid+label:after,input[type=number]:focus.valid+label:after,input[type=search].valid+label:after,input[type=search]:focus.valid+label:after,textarea.materialize-textarea.valid+label:after,textarea.materialize-textarea:focus.valid+label:after{content:attr(data-success);color:#4CAF50;opacity:1}input:not([type]).invalid,input:not([type]):focus.invalid,input[type=text].invalid,input[type=text]:focus.invalid,input[type=password].invalid,input[type=password]:focus.invalid,input[type=email].invalid,input[type=email]:focus.invalid,input[type=url].invalid,input[type=url]:focus.invalid,input[type=time].invalid,input[type=time]:focus.invalid,input[type=date].invalid,input[type=date]:focus.invalid,input[type=datetime].invalid,input[type=datetime]:focus.invalid,input[type=datetime-local].invalid,input[type=datetime-local]:focus.invalid,input[type=tel].invalid,input[type=tel]:focus.invalid,input[type=number].invalid,input[type=number]:focus.invalid,input[type=search].invalid,input[type=search]:focus.invalid,textarea.materialize-textarea.invalid,textarea.materialize-textarea:focus.invalid{border-bottom:1px solid #F44336;box-shadow:0 1px 0 0 #F44336}input:not([type]).invalid+label:after,input:not([type]):focus.invalid+label:after,input[type=text].invalid+label:after,input[type=text]:focus.invalid+label:after,input[type=password].invalid+label:after,input[type=password]:focus.invalid+label:after,input[type=email].invalid+label:after,input[type=email]:focus.invalid+label:after,input[type=url].invalid+label:after,input[type=url]:focus.invalid+label:after,input[type=time].invalid+label:after,input[type=time]:focus.invalid+label:after,input[type=date].invalid+label:after,input[type=date]:focus.invalid+label:after,input[type=datetime].invalid+label:after,input[type=datetime]:focus.invalid+label:after,input[type=datetime-local].invalid+label:after,input[type=datetime-local]:focus.invalid+label:after,input[type=tel].invalid+label:after,input[type=tel]:focus.invalid+label:after,input[type=number].invalid+label:after,input[type=number]:focus.invalid+label:after,input[type=search].invalid+label:after,input[type=search]:focus.invalid+label:after,textarea.materialize-textarea.invalid+label:after,textarea.materialize-textarea:focus.invalid+label:after{content:attr(data-error);color:#F44336;opacity:1}input:not([type]).validate+label,input[type=text].validate+label,input[type=password].validate+label,input[type=email].validate+label,input[type=url].validate+label,input[type=time].validate+label,input[type=date].validate+label,input[type=datetime].validate+label,input[type=datetime-local].validate+label,input[type=tel].validate+label,input[type=number].validate+label,input[type=search].validate+label,textarea.materialize-textarea.validate+label{width:100%;pointer-events:none}input:not([type])+label:after,input[type=text]+label:after,input[type=password]+label:after,input[type=email]+label:after,input[type=url]+label:after,input[type=time]+label:after,input[type=date]+label:after,input[type=datetime]+label:after,input[type=datetime-local]+label:after,input[type=tel]+label:after,input[type=number]+label:after,input[type=search]+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:65px;opacity:0;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem}.input-field label{color:#9e9e9e;position:absolute;top:0.8rem;left:.75rem;font-size:1rem;cursor:text;transition:.2s ease-out}.input-field label.active{font-size:.8rem;transform:translateY(-140%)}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;transition:color .2s}.input-field .prefix.active{color:#26a69a}.input-field .prefix ~ input,.input-field .prefix ~ textarea{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix ~ textarea{padding-top:.8rem}.input-field .prefix ~ label{margin-left:3rem}@media only screen and (max-width: 992px){.input-field .prefix ~ input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width: 600px){.input-field .prefix ~ input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;padding-left:4rem;width:calc(100% - 4rem)}.input-field input[type=search]:focus{background-color:#fff;border:0;box-shadow:none;color:#444}.input-field input[type=search]:focus+label i,.input-field input[type=search]:focus ~ .mdi-navigation-close,.input-field input[type=search]:focus ~ .material-icons{color:#444}.input-field input[type=search]+label{left:1rem}.input-field input[type=search] ~ .mdi-navigation-close,.input-field input[type=search] ~ .material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{overflow-y:hidden;padding:1.6rem 0;resize:none;min-height:3rem}.hiddendiv{display:none;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;left:-9999px;opacity:0}[type="radio"]:not(:checked)+label,[type="radio"]:checked+label{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;-khtml-user-select:none;user-select:none}[type="radio"]+label:before,[type="radio"]+label:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type="radio"]:not(:checked)+label:before,[type="radio"]:not(:checked)+label:after,[type="radio"]:checked+label:before,[type="radio"]:checked+label:after,[type="radio"].with-gap:checked+label:before,[type="radio"].with-gap:checked+label:after{border-radius:50%}[type="radio"]:not(:checked)+label:before,[type="radio"]:not(:checked)+label:after{border:2px solid #5a5a5a}[type="radio"]:not(:checked)+label:after{z-index:-1;transform:scale(0)}[type="radio"]:checked+label:before{border:2px solid transparent}[type="radio"]:checked+label:after,[type="radio"].with-gap:checked+label:before,[type="radio"].with-gap:checked+label:after{border:2px solid #26a69a}[type="radio"]:checked+label:after,[type="radio"].with-gap:checked+label:after{background-color:#26a69a;z-index:0}[type="radio"]:checked+label:after{transform:scale(1.02)}[type="radio"].with-gap:checked+label:after{transform:scale(0.5)}[type="radio"].tabbed:focus+label:before{box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+label:before{border:2px solid rgba(0,0,0,0.26)}[type="radio"].with-gap:disabled:checked+label:after{border:none;background-color:rgba(0,0,0,0.26)}[type="radio"]:disabled:not(:checked)+label:before,[type="radio"]:disabled:checked+label:before{background-color:transparent;border-color:rgba(0,0,0,0.26)}[type="radio"]:disabled+label{color:rgba(0,0,0,0.26)}[type="radio"]:disabled:not(:checked)+label:before{border-color:rgba(0,0,0,0.26)}[type="radio"]:disabled:checked+label:after{background-color:rgba(0,0,0,0.26);border-color:#BDBDBD}form p{margin-bottom:10px;text-align:left}form p:last-child{margin-bottom:0}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;left:-9999px;opacity:0}[type="checkbox"]+label{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-ms-user-select:none}[type="checkbox"]+label:before,[type="checkbox"]:not(.filled-in)+label:after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:2px;transition:.2s}[type="checkbox"]:not(.filled-in)+label:after{border:0;transform:scale(0)}[type="checkbox"]:not(:checked):disabled+label:before{border:none;background-color:rgba(0,0,0,0.26)}[type="checkbox"].tabbed:focus+label:after{transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+label:before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;transform:rotate(40deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+label:before{border-right:2px solid rgba(0,0,0,0.26);border-bottom:2px solid rgba(0,0,0,0.26)}[type="checkbox"]:indeterminate+label:before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;transform:rotate(90deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+label:before{border-right:2px solid rgba(0,0,0,0.26);background-color:transparent}[type="checkbox"].filled-in+label:after{border-radius:2px}[type="checkbox"].filled-in+label:before,[type="checkbox"].filled-in+label:after{content:'';left:0;position:absolute;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+label:before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:20% 40%;transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+label:after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0px;z-index:0}[type="checkbox"].filled-in:checked+label:before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:checked+label:after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type="checkbox"].filled-in.tabbed:focus+label:after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+label:after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type="checkbox"].filled-in:disabled:not(:checked)+label:before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+label:after{border-color:transparent;background-color:#BDBDBD}[type="checkbox"].filled-in:disabled:checked+label:before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+label:after{background-color:#BDBDBD;border-color:#BDBDBD}.switch,.switch *{-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-ms-user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a;left:24px}.switch label .lever{content:"";display:inline-block;position:relative;width:40px;height:15px;background-color:#818181;border-radius:15px;margin-right:10px;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:after{content:"";position:absolute;display:inline-block;width:21px;height:21px;background-color:#F1F1F1;border-radius:21px;box-shadow:0 1px 3px 1px rgba(0,0,0,0.4);left:-5px;top:-3px;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease}input[type=checkbox]:checked:not(:disabled) ~ .lever:active::after,input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::after{box-shadow:0 1px 3px 1px rgba(0,0,0,0.4),0 0 0 15px rgba(38,166,154,0.1)}input[type=checkbox]:not(:disabled) ~ .lever:active:after,input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::after{box-shadow:0 1px 3px 1px rgba(0,0,0,0.4),0 0 0 15px rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#BDBDBD}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:1rem;margin:0 0 15px 0;padding:0;display:block}.select-wrapper span.caret{color:initial;position:absolute;right:0;top:16px;font-size:10px}.select-wrapper span.caret.disabled{color:rgba(0,0,0,0.26)}.select-wrapper+label{position:absolute;top:-14px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.3)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.3);cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;border-bottom:1px solid rgba(0,0,0,0.3)}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}.prefix ~ .select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix ~ label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup ~ li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;top:10px;margin-left:-6px;transform-origin:50% 50%;transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;border:none;height:14px;width:14px;border-radius:50%;background-color:#26a69a;transform-origin:50% 50%;margin:-5px 0 0 0;transition:.3s}input[type=range]:focus::-webkit-slider-runnable-track{background:#ccc}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#ddd;border:none}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}input[type=range]:focus::-moz-range-track{background:#ccc}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a}input[type=range]:focus::-ms-fill-lower{background:#888}input[type=range]:focus::-ms-fill-upper{background:#ccc}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:20px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:19px;border-left:1px solid #ea4a4f}.table-of-contents a.active{font-weight:500;padding-left:18px;border-left:2px solid #ea4a4f}.side-nav{position:fixed;width:240px;left:0;top:0;margin:0;transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;backface-visibility:hidden;overflow-y:auto;will-change:transform;backface-visibility:hidden;transform:translateX(-105%)}.side-nav.right-aligned{right:0;transform:translateX(105%);left:auto;transform:translateX(100%)}.side-nav .collapsible{margin:0}.side-nav li{float:none;line-height:64px}.side-nav li.active{background-color:rgba(0,0,0,0.05)}.side-nav a{color:#444;display:block;font-size:1rem;height:64px;line-height:64px;padding:0 30px}.side-nav a:hover{background-color:rgba(0,0,0,0.05)}.side-nav a.btn,.side-nav a.btn-large,.side-nav a.btn-large,.side-nav a.btn-flat,.side-nav a.btn-floating{margin:10px 15px}.side-nav a.btn,.side-nav a.btn-large,.side-nav a.btn-large,.side-nav a.btn-floating{color:#fff}.side-nav a.btn-flat{color:#343434}.side-nav a.btn:hover,.side-nav a.btn-large:hover,.side-nav a.btn-large:hover{background-color:#2bbbad}.side-nav a.btn-floating:hover{background-color:#26a69a}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.side-nav.fixed a{display:block;padding:0 30px;color:#444}.side-nav.fixed{left:0;transform:translateX(0);position:fixed}.side-nav.fixed.right-aligned{right:0;left:auto}@media only screen and (max-width: 992px){.side-nav.fixed{transform:translateX(-105%)}.side-nav.fixed.right-aligned{transform:translateX(105%)}}.side-nav .collapsible-body li.active,.side-nav.fixed .collapsible-body li.active{background-color:#ee6e73}.side-nav .collapsible-body li.active a,.side-nav.fixed .collapsible-body li.active a{color:#fff}#sidenav-overlay{position:fixed;top:0;left:0;right:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;will-change:opacity}.preloader-wrapper{display:inline-block;position:relative;width:48px;height:48px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}to{transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{transform:rotate(130deg)}50%{transform:rotate(-5deg)}to{transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{transform:rotate(-130deg)}50%{transform:rotate(5deg)}to{transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;perspective:500px;transform-style:preserve-3d;transform-origin:0% 50%}.carousel .carousel-item{width:200px;position:absolute;top:0;left:0}.carousel .carousel-item img{width:100%}.carousel.carousel-slider{top:0;left:0;height:0}.carousel.carousel-slider .carousel-item{width:100%;height:100%;position:absolute;top:0;left:0}.picker{font-size:16px;text-align:left;line-height:1.2;color:#000000;position:absolute;z-index:10000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.picker__input{cursor:default}.picker__input.picker__input--active{border-color:#0089ec}.picker__holder{width:100%;overflow-y:auto;-webkit-overflow-scrolling:touch}/*!
+ * Default mobile-first, responsive styling for pickadate.js
+ * Demo: http://amsul.github.io/pickadate.js
+ */.picker__holder,.picker__frame{bottom:0;left:0;right:0;top:100%}.picker__holder{position:fixed;-webkit-transition:background 0.15s ease-out, top 0s 0.15s;-moz-transition:background 0.15s ease-out, top 0s 0.15s;transition:background 0.15s ease-out, top 0s 0.15s;-webkit-backface-visibility:hidden}.picker__frame{position:absolute;margin:0 auto;min-width:256px;width:300px;max-height:350px;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";filter:alpha(opacity=0);-moz-opacity:0;opacity:0;-webkit-transition:all 0.15s ease-out;-moz-transition:all 0.15s ease-out;transition:all 0.15s ease-out}@media (min-height: 28.875em){.picker__frame{overflow:visible;top:auto;bottom:-100%;max-height:80%}}@media (min-height: 40.125em){.picker__frame{margin-bottom:7.5%}}.picker__wrap{display:table;width:100%;height:100%}@media (min-height: 28.875em){.picker__wrap{display:block}}.picker__box{background:#ffffff;display:table-cell;vertical-align:middle}@media (min-height: 28.875em){.picker__box{display:block;border:1px solid #777777;border-top-color:#898989;border-bottom-width:0;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0;-webkit-box-shadow:0 12px 36px 16px rgba(0,0,0,0.24);-moz-box-shadow:0 12px 36px 16px rgba(0,0,0,0.24);box-shadow:0 12px 36px 16px rgba(0,0,0,0.24)}}.picker--opened .picker__holder{top:0;background:transparent;-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#1E000000,endColorstr=#1E000000)";zoom:1;background:rgba(0,0,0,0.32);-webkit-transition:background 0.15s ease-out;-moz-transition:background 0.15s ease-out;transition:background 0.15s ease-out}.picker--opened .picker__frame{top:0;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);-moz-opacity:1;opacity:1}@media (min-height: 35.875em){.picker--opened .picker__frame{top:10%;bottom:auto}}.picker__input.picker__input--active{border-color:#E3F2FD}.picker__frame{margin:0 auto;max-width:325px}@media (min-height: 38.875em){.picker--opened .picker__frame{top:10%;bottom:auto}}.picker__box{padding:0 1em}.picker__header{text-align:center;position:relative;margin-top:.75em}.picker__month,.picker__year{display:inline-block;margin-left:.25em;margin-right:.25em}.picker__select--month,.picker__select--year{height:2em;padding:0;margin-left:.25em;margin-right:.25em}.picker__select--month.browser-default{display:inline;background-color:#FFFFFF;width:40%}.picker__select--year.browser-default{display:inline;background-color:#FFFFFF;width:26%}.picker__select--month:focus,.picker__select--year:focus{border-color:rgba(0,0,0,0.05)}.picker__nav--prev,.picker__nav--next{position:absolute;padding:.5em 1.25em;width:1em;height:1em;box-sizing:content-box;top:-0.25em}.picker__nav--prev{left:-1em;padding-right:1.25em}.picker__nav--next{right:-1em;padding-left:1.25em}.picker__nav--disabled,.picker__nav--disabled:hover,.picker__nav--disabled:before,.picker__nav--disabled:before:hover{cursor:default;background:none;border-right-color:#f5f5f5;border-left-color:#f5f5f5}.picker__table{text-align:center;border-collapse:collapse;border-spacing:0;table-layout:fixed;font-size:1rem;width:100%;margin-top:.75em;margin-bottom:.5em}.picker__table th,.picker__table td{text-align:center}.picker__table td{margin:0;padding:0}.picker__weekday{width:14.285714286%;font-size:.75em;padding-bottom:.25em;color:#999999;font-weight:500}@media (min-height: 33.875em){.picker__weekday{padding-bottom:.5em}}.picker__day--today{position:relative;color:#595959;letter-spacing:-.3;padding:.75rem 0;font-weight:400;border:1px solid transparent}.picker__day--disabled:before{border-top-color:#aaaaaa}.picker__day--infocus:hover{cursor:pointer;color:#000;font-weight:500}.picker__day--outfocus{display:none;padding:.75rem 0;color:#fff}.picker__day--outfocus:hover{cursor:pointer;color:#dddddd;font-weight:500}.picker__day--highlighted:hover,.picker--focused .picker__day--highlighted{cursor:pointer}.picker__day--selected,.picker__day--selected:hover,.picker--focused .picker__day--selected{border-radius:50%;transform:scale(0.75);background:#0089ec;color:#ffffff}.picker__day--disabled,.picker__day--disabled:hover,.picker--focused .picker__day--disabled{background:#f5f5f5;border-color:#f5f5f5;color:#dddddd;cursor:default}.picker__day--highlighted.picker__day--disabled,.picker__day--highlighted.picker__day--disabled:hover{background:#bbbbbb}.picker__footer{text-align:center;display:flex;align-items:center;justify-content:space-between}.picker__button--today,.picker__button--clear,.picker__button--close{border:1px solid #ffffff;background:#ffffff;font-size:.8em;padding:.66em 0;font-weight:bold;width:33%;display:inline-block;vertical-align:bottom}.picker__button--today:hover,.picker__button--clear:hover,.picker__button--close:hover{cursor:pointer;color:#000000;background:#b1dcfb;border-bottom-color:#b1dcfb}.picker__button--today:focus,.picker__button--clear:focus,.picker__button--close:focus{background:#b1dcfb;border-color:rgba(0,0,0,0.05);outline:none}.picker__button--today:before,.picker__button--clear:before,.picker__button--close:before{position:relative;display:inline-block;height:0}.picker__button--today:before,.picker__button--clear:before{content:" ";margin-right:.45em}.picker__button--today:before{top:-0.05em;width:0;border-top:0.66em solid #0059bc;border-left:.66em solid transparent}.picker__button--clear:before{top:-0.25em;width:.66em;border-top:3px solid #ee2200}.picker__button--close:before{content:"\D7";top:-0.1em;vertical-align:top;font-size:1.1em;margin-right:.35em;color:#777777}.picker__button--today[disabled],.picker__button--today[disabled]:hover{background:#f5f5f5;border-color:#f5f5f5;color:#dddddd;cursor:default}.picker__button--today[disabled]:before{border-top-color:#aaaaaa}.picker__box{border-radius:2px;overflow:hidden}.picker__date-display{text-align:center;background-color:#26a69a;color:#fff;padding-bottom:15px;font-weight:300}.picker__nav--prev:hover,.picker__nav--next:hover{cursor:pointer;color:#000000;background:#a1ded8}.picker__weekday-display{background-color:#1f897f;padding:10px;font-weight:200;letter-spacing:.5;font-size:1rem;margin-bottom:15px}.picker__month-display{text-transform:uppercase;font-size:2rem}.picker__day-display{font-size:4.5rem;font-weight:400}.picker__year-display{font-size:1.8rem;color:rgba(255,255,255,0.4)}.picker__box{padding:0}.picker__calendar-container{padding:0 1rem}.picker__calendar-container thead{border:none}.picker__table{margin-top:0;margin-bottom:.5em}.picker__day--infocus{color:#595959;letter-spacing:-.3;padding:.75rem 0;font-weight:400;border:1px solid transparent}.picker__day.picker__day--today{color:#26a69a}.picker__day.picker__day--today.picker__day--selected{color:#fff}.picker__weekday{font-size:.9rem}.picker__day--selected,.picker__day--selected:hover,.picker--focused .picker__day--selected{border-radius:50%;transform:scale(0.9);background-color:#26a69a;color:#ffffff}.picker__day--selected.picker__day--outfocus,.picker__day--selected:hover.picker__day--outfocus,.picker--focused .picker__day--selected.picker__day--outfocus{background-color:#a1ded8}.picker__footer{text-align:right;padding:5px 10px}.picker__close,.picker__today{font-size:1.1rem;padding:0 1rem;color:#26a69a}.picker__nav--prev:before,.picker__nav--next:before{content:" ";border-top:.5em solid transparent;border-bottom:.5em solid transparent;border-right:0.75em solid #676767;width:0;height:0;display:block;margin:0 auto}.picker__nav--next:before{border-right:0;border-left:0.75em solid #676767}button.picker__today:focus,button.picker__clear:focus,button.picker__close:focus{background-color:#a1ded8}.picker__list{list-style:none;padding:0.75em 0 4.2em;margin:0}.picker__list-item{border-bottom:1px solid #dddddd;border-top:1px solid #dddddd;margin-bottom:-1px;position:relative;background:#ffffff;padding:.75em 1.25em}@media (min-height: 46.75em){.picker__list-item{padding:.5em 1em}}.picker__list-item:hover{cursor:pointer;color:#000000;background:#b1dcfb;border-color:#0089ec;z-index:10}.picker__list-item--highlighted{border-color:#0089ec;z-index:10}.picker__list-item--highlighted:hover,.picker--focused .picker__list-item--highlighted{cursor:pointer;color:#000000;background:#b1dcfb}.picker__list-item--selected,.picker__list-item--selected:hover,.picker--focused .picker__list-item--selected{background:#0089ec;color:#ffffff;z-index:10}.picker__list-item--disabled,.picker__list-item--disabled:hover,.picker--focused .picker__list-item--disabled{background:#f5f5f5;border-color:#f5f5f5;color:#dddddd;cursor:default;border-color:#dddddd;z-index:auto}.picker--time .picker__button--clear{display:block;width:80%;margin:1em auto 0;padding:1em 1.25em;background:none;border:0;font-weight:500;font-size:.67em;text-align:center;text-transform:uppercase;color:#666}.picker--time .picker__button--clear:hover,.picker--time .picker__button--clear:focus{color:#000000;background:#b1dcfb;background:#ee2200;border-color:#ee2200;cursor:pointer;color:#ffffff;outline:none}.picker--time .picker__button--clear:before{top:-0.25em;color:#666;font-size:1.25em;font-weight:bold}.picker--time .picker__button--clear:hover:before,.picker--time .picker__button--clear:focus:before{color:#ffffff}.picker--time .picker__frame{min-width:256px;max-width:320px}.picker--time .picker__box{font-size:1em;background:#f2f2f2;padding:0}@media (min-height: 40.125em){.picker--time .picker__box{margin-bottom:5em}}
diff --git a/docs/assets/css/materialize.scss b/docs/assets/css/materialize.scss
new file mode 100644
index 0000000..372de9d
--- /dev/null
+++ b/docs/assets/css/materialize.scss
@@ -0,0 +1,42 @@
+---
+# This is the Front Matter block
+---
+
+// Mixins
+// @import "components/prefixer";
+@import "components/mixins";
+@import "components/color";
+
+// Variables;
+@import "components/variables";
+
+// Reset
+@import "components/normalize";
+
+// components
+@import "components/global";
+@import "components/icons-material-design";
+@import "components/grid";
+@import "components/navbar";
+@import "components/roboto";
+@import "components/typography";
+@import "components/cards";
+@import "components/toast";
+@import "components/tabs";
+@import "components/tooltip";
+@import "components/buttons";
+@import "components/dropdown";
+@import "components/waves";
+@import "components/modal";
+@import "components/collapsible";
+@import "components/chips";
+@import "components/materialbox";
+@import "components/forms/forms";
+@import "components/table_of_contents";
+@import "components/sideNav";
+@import "components/preloader";
+@import "components/slider";
+@import "components/carousel";
+@import "components/date_picker/default";
+@import "components/date_picker/default.date";
+@import "components/date_picker/default.time";
diff --git a/docs/assets/css/styles.css b/docs/assets/css/styles.css
new file mode 100644
index 0000000..85f0ae6
--- /dev/null
+++ b/docs/assets/css/styles.css
@@ -0,0 +1,89 @@
+h1 {
+ font-size : 2.5rem !important;
+}
+h2 {
+ font-size : 2rem !important;
+}
+h3 {
+ font-size : 1.5rem !important;
+}
+h1,h2,h3,h4,h5 {
+ color : #95C12B !important;
+}
+.z-depth-1, nav, .card-panel, .card, .toast, .btn, .btn-large, .btn-floating, .dropdown-content, .collapsible, .side-nav {
+ box-shadow: 0 2px 5px 0 rgba(149,193,43,0.16), 0 2px 10px 0 rgba(0,0,0,0.12) !important;
+}
+.toctext{
+ color : #000000 !important;
+}
+#div_content ul {
+ list-style-type: initial !important;
+}
+#div_content ul li {
+ list-style-type: initial !important;
+ margin-left : 20px !important;
+}
+#div_content {
+ padding-left : 400px !important;
+}
+#side-nav {
+ background-color:#F3F3F3 !important;
+ z-index:2 !important;
+ top:60px !important;
+ width : 350px !important;
+}
+#div_menuMobile {
+ background-color:#F3F3F3 !important;
+}
+@media only screen and (max-width : 992px) {
+ #div_content {
+ padding-left : 0 !important;
+ }
+}
+.toclevel-2{
+ margin-left : 10px !important;
+}
+nav ul a,nav {
+ color: #444 !important;
+ background-color: #fff !important;
+}
+.jeedomcolorbg {
+ background-color: #95C12B !important;
+}
+.jeedomcolor {
+ color: #95C12B !important;
+}
+.collection a.collection-item {
+ color: #85a835 !important;
+}
+.container {
+ @media only screen and (min-width: 993px){
+ width: 85% !important;
+ }
+}
+.waves-effect.waves-jeedom .waves-ripple {
+ background-color: rgba(149, 193, 43, 0.65);
+}
+.toclevel-1 a{
+ padding-left: 10px !important;
+ height: 100% !important;
+ vertical-align: middle !important;
+}
+.toclevel-1{
+ vertical-align: middle !important;
+}
+.toctext{
+ font-size : 0.85rem !important;
+}
+.tocnumber{
+ color : #95C12B !important;
+}
+#side-nav a{
+ line-height: 24px !important;
+}
+.toc a.active{
+ font-weight: bold;
+}
+.dropdown-content li>a, .dropdown-content li>span{
+ color : #95C12B !important;
+}
\ No newline at end of file
diff --git a/docs/assets/font/material-design-icons/LICENSE.txt b/docs/assets/font/material-design-icons/LICENSE.txt
new file mode 100644
index 0000000..542f653
--- /dev/null
+++ b/docs/assets/font/material-design-icons/LICENSE.txt
@@ -0,0 +1,428 @@
+https://github.com/google/material-design-icons/blob/master/LICENSE
+https://github.com/FezVrasta/bootstrap-material-design/blob/master/fonts/LICENSE.txt
+
+Attribution-ShareAlike 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+ Considerations for licensors: Our public licenses are
+ intended for use by those authorized to give the public
+ permission to use material in ways otherwise restricted by
+ copyright and certain other rights. Our licenses are
+ irrevocable. Licensors should read and understand the terms
+ and conditions of the license they choose before applying it.
+ Licensors should also secure all rights necessary before
+ applying our licenses so that the public can reuse the
+ material as expected. Licensors should clearly mark any
+ material not subject to the license. This includes other CC-
+ licensed material, or material used under an exception or
+ limitation to copyright. More considerations for licensors:
+ wiki.creativecommons.org/Considerations_for_licensors
+
+ Considerations for the public: By using one of our public
+ licenses, a licensor grants the public permission to use the
+ licensed material under specified terms and conditions. If
+ the licensor's permission is not necessary for any reason--for
+ example, because of any applicable exception or limitation to
+ copyright--then that use is not regulated by the license. Our
+ licenses grant only permissions under copyright and certain
+ other rights that a licensor has authority to grant. Use of
+ the licensed material may still be restricted for other
+ reasons, including because others have copyright or other
+ rights in the material. A licensor may make special requests,
+ such as asking that all changes be marked or described.
+ Although not required by our licenses, you are encouraged to
+ respect those requests where reasonable. More_considerations
+ for the public:
+ wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution-ShareAlike 4.0 International Public
+License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution-ShareAlike 4.0 International Public License ("Public
+License"). To the extent this Public License may be interpreted as a
+contract, You are granted the Licensed Rights in consideration of Your
+acceptance of these terms and conditions, and the Licensor grants You
+such rights in consideration of benefits the Licensor receives from
+making the Licensed Material available under these terms and
+conditions.
+
+
+Section 1 -- Definitions.
+
+ a. Adapted Material means material subject to Copyright and Similar
+ Rights that is derived from or based upon the Licensed Material
+ and in which the Licensed Material is translated, altered,
+ arranged, transformed, or otherwise modified in a manner requiring
+ permission under the Copyright and Similar Rights held by the
+ Licensor. For purposes of this Public License, where the Licensed
+ Material is a musical work, performance, or sound recording,
+ Adapted Material is always produced where the Licensed Material is
+ synched in timed relation with a moving image.
+
+ b. Adapter's License means the license You apply to Your Copyright
+ and Similar Rights in Your contributions to Adapted Material in
+ accordance with the terms and conditions of this Public License.
+
+ c. BY-SA Compatible License means a license listed at
+ creativecommons.org/compatiblelicenses, approved by Creative
+ Commons as essentially the equivalent of this Public License.
+
+ d. Copyright and Similar Rights means copyright and/or similar rights
+ closely related to copyright including, without limitation,
+ performance, broadcast, sound recording, and Sui Generis Database
+ Rights, without regard to how the rights are labeled or
+ categorized. For purposes of this Public License, the rights
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
+ Rights.
+
+ e. Effective Technological Measures means those measures that, in the
+ absence of proper authority, may not be circumvented under laws
+ fulfilling obligations under Article 11 of the WIPO Copyright
+ Treaty adopted on December 20, 1996, and/or similar international
+ agreements.
+
+ f. Exceptions and Limitations means fair use, fair dealing, and/or
+ any other exception or limitation to Copyright and Similar Rights
+ that applies to Your use of the Licensed Material.
+
+ g. License Elements means the license attributes listed in the name
+ of a Creative Commons Public License. The License Elements of this
+ Public License are Attribution and ShareAlike.
+
+ h. Licensed Material means the artistic or literary work, database,
+ or other material to which the Licensor applied this Public
+ License.
+
+ i. Licensed Rights means the rights granted to You subject to the
+ terms and conditions of this Public License, which are limited to
+ all Copyright and Similar Rights that apply to Your use of the
+ Licensed Material and that the Licensor has authority to license.
+
+ j. Licensor means the individual(s) or entity(ies) granting rights
+ under this Public License.
+
+ k. Share means to provide material to the public by any means or
+ process that requires permission under the Licensed Rights, such
+ as reproduction, public display, public performance, distribution,
+ dissemination, communication, or importation, and to make material
+ available to the public including in ways that members of the
+ public may access the material from a place and at a time
+ individually chosen by them.
+
+ l. Sui Generis Database Rights means rights other than copyright
+ resulting from Directive 96/9/EC of the European Parliament and of
+ the Council of 11 March 1996 on the legal protection of databases,
+ as amended and/or succeeded, as well as other essentially
+ equivalent rights anywhere in the world.
+
+ m. You means the individual or entity exercising the Licensed Rights
+ under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+ a. License grant.
+
+ 1. Subject to the terms and conditions of this Public License,
+ the Licensor hereby grants You a worldwide, royalty-free,
+ non-sublicensable, non-exclusive, irrevocable license to
+ exercise the Licensed Rights in the Licensed Material to:
+
+ a. reproduce and Share the Licensed Material, in whole or
+ in part; and
+
+ b. produce, reproduce, and Share Adapted Material.
+
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
+ Exceptions and Limitations apply to Your use, this Public
+ License does not apply, and You do not need to comply with
+ its terms and conditions.
+
+ 3. Term. The term of this Public License is specified in Section
+ 6(a).
+
+ 4. Media and formats; technical modifications allowed. The
+ Licensor authorizes You to exercise the Licensed Rights in
+ all media and formats whether now known or hereafter created,
+ and to make technical modifications necessary to do so. The
+ Licensor waives and/or agrees not to assert any right or
+ authority to forbid You from making technical modifications
+ necessary to exercise the Licensed Rights, including
+ technical modifications necessary to circumvent Effective
+ Technological Measures. For purposes of this Public License,
+ simply making modifications authorized by this Section 2(a)
+ (4) never produces Adapted Material.
+
+ 5. Downstream recipients.
+
+ a. Offer from the Licensor -- Licensed Material. Every
+ recipient of the Licensed Material automatically
+ receives an offer from the Licensor to exercise the
+ Licensed Rights under the terms and conditions of this
+ Public License.
+
+ b. Additional offer from the Licensor -- Adapted Material.
+ Every recipient of Adapted Material from You
+ automatically receives an offer from the Licensor to
+ exercise the Licensed Rights in the Adapted Material
+ under the conditions of the Adapter's License You apply.
+
+ c. No downstream restrictions. You may not offer or impose
+ any additional or different terms or conditions on, or
+ apply any Effective Technological Measures to, the
+ Licensed Material if doing so restricts exercise of the
+ Licensed Rights by any recipient of the Licensed
+ Material.
+
+ 6. No endorsement. Nothing in this Public License constitutes or
+ may be construed as permission to assert or imply that You
+ are, or that Your use of the Licensed Material is, connected
+ with, or sponsored, endorsed, or granted official status by,
+ the Licensor or others designated to receive attribution as
+ provided in Section 3(a)(1)(A)(i).
+
+ b. Other rights.
+
+ 1. Moral rights, such as the right of integrity, are not
+ licensed under this Public License, nor are publicity,
+ privacy, and/or other similar personality rights; however, to
+ the extent possible, the Licensor waives and/or agrees not to
+ assert any such rights held by the Licensor to the limited
+ extent necessary to allow You to exercise the Licensed
+ Rights, but not otherwise.
+
+ 2. Patent and trademark rights are not licensed under this
+ Public License.
+
+ 3. To the extent possible, the Licensor waives any right to
+ collect royalties from You for the exercise of the Licensed
+ Rights, whether directly or through a collecting society
+ under any voluntary or waivable statutory or compulsory
+ licensing scheme. In all other cases the Licensor expressly
+ reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+ a. Attribution.
+
+ 1. If You Share the Licensed Material (including in modified
+ form), You must:
+
+ a. retain the following if it is supplied by the Licensor
+ with the Licensed Material:
+
+ i. identification of the creator(s) of the Licensed
+ Material and any others designated to receive
+ attribution, in any reasonable manner requested by
+ the Licensor (including by pseudonym if
+ designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of
+ warranties;
+
+ v. a URI or hyperlink to the Licensed Material to the
+ extent reasonably practicable;
+
+ b. indicate if You modified the Licensed Material and
+ retain an indication of any previous modifications; and
+
+ c. indicate the Licensed Material is licensed under this
+ Public License, and include the text of, or the URI or
+ hyperlink to, this Public License.
+
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
+ reasonable manner based on the medium, means, and context in
+ which You Share the Licensed Material. For example, it may be
+ reasonable to satisfy the conditions by providing a URI or
+ hyperlink to a resource that includes the required
+ information.
+
+ 3. If requested by the Licensor, You must remove any of the
+ information required by Section 3(a)(1)(A) to the extent
+ reasonably practicable.
+
+ b. ShareAlike.
+
+ In addition to the conditions in Section 3(a), if You Share
+ Adapted Material You produce, the following conditions also apply.
+
+ 1. The Adapter's License You apply must be a Creative Commons
+ license with the same License Elements, this version or
+ later, or a BY-SA Compatible License.
+
+ 2. You must include the text of, or the URI or hyperlink to, the
+ Adapter's License You apply. You may satisfy this condition
+ in any reasonable manner based on the medium, means, and
+ context in which You Share Adapted Material.
+
+ 3. You may not offer or impose any additional or different terms
+ or conditions on, or apply any Effective Technological
+ Measures to, Adapted Material that restrict exercise of the
+ rights granted under the Adapter's License You apply.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+ to extract, reuse, reproduce, and Share all or a substantial
+ portion of the contents of the database;
+
+ b. if You include all or a substantial portion of the database
+ contents in a database in which You have Sui Generis Database
+ Rights, then the database in which You have Sui Generis Database
+ Rights (but not its individual contents) is Adapted Material,
+
+ including for purposes of Section 3(b); and
+ c. You must comply with the conditions in Section 3(a) if You Share
+ all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+ c. The disclaimer of warranties and limitation of liability provided
+ above shall be interpreted in a manner that, to the extent
+ possible, most closely approximates an absolute disclaimer and
+ waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+ a. This Public License applies for the term of the Copyright and
+ Similar Rights licensed here. However, if You fail to comply with
+ this Public License, then Your rights under this Public License
+ terminate automatically.
+
+ b. Where Your right to use the Licensed Material has terminated under
+ Section 6(a), it reinstates:
+
+ 1. automatically as of the date the violation is cured, provided
+ it is cured within 30 days of Your discovery of the
+ violation; or
+
+ 2. upon express reinstatement by the Licensor.
+
+ For the avoidance of doubt, this Section 6(b) does not affect any
+ right the Licensor may have to seek remedies for Your violations
+ of this Public License.
+
+ c. For the avoidance of doubt, the Licensor may also offer the
+ Licensed Material under separate terms or conditions or stop
+ distributing the Licensed Material at any time; however, doing so
+ will not terminate this Public License.
+
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+ License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+ a. The Licensor shall not be bound by any additional or different
+ terms or conditions communicated by You unless expressly agreed.
+
+ b. Any arrangements, understandings, or agreements regarding the
+ Licensed Material not stated herein are separate from and
+ independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+ a. For the avoidance of doubt, this Public License does not, and
+ shall not be interpreted to, reduce, limit, restrict, or impose
+ conditions on any use of the Licensed Material that could lawfully
+ be made without permission under this Public License.
+
+ b. To the extent possible, if any provision of this Public License is
+ deemed unenforceable, it shall be automatically reformed to the
+ minimum extent necessary to make it enforceable. If the provision
+ cannot be reformed, it shall be severed from this Public License
+ without affecting the enforceability of the remaining terms and
+ conditions.
+
+ c. No term or condition of this Public License will be waived and no
+ failure to comply consented to unless expressly agreed to by the
+ Licensor.
+
+ d. Nothing in this Public License constitutes or may be interpreted
+ as a limitation upon, or waiver of, any privileges and immunities
+ that apply to the Licensor or You, including from the legal
+ processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public licenses.
+Notwithstanding, Creative Commons may elect to apply one of its public
+licenses to material it publishes and in those instances will be
+considered the "Licensor." Except for the limited purpose of indicating
+that material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the public
+licenses.
+
+Creative Commons may be contacted at creativecommons.org.
diff --git a/docs/assets/font/material-design-icons/Material-Design-Icons.eot b/docs/assets/font/material-design-icons/Material-Design-Icons.eot
new file mode 100644
index 0000000..d9c296e
Binary files /dev/null and b/docs/assets/font/material-design-icons/Material-Design-Icons.eot differ
diff --git a/docs/assets/font/material-design-icons/Material-Design-Icons.svg b/docs/assets/font/material-design-icons/Material-Design-Icons.svg
new file mode 100644
index 0000000..def1e9b
--- /dev/null
+++ b/docs/assets/font/material-design-icons/Material-Design-Icons.svg
@@ -0,0 +1,769 @@
+
+
+
+Generated by IcoMoon
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/font/material-design-icons/Material-Design-Icons.ttf b/docs/assets/font/material-design-icons/Material-Design-Icons.ttf
new file mode 100644
index 0000000..e12f0cc
Binary files /dev/null and b/docs/assets/font/material-design-icons/Material-Design-Icons.ttf differ
diff --git a/docs/assets/font/material-design-icons/Material-Design-Icons.woff b/docs/assets/font/material-design-icons/Material-Design-Icons.woff
new file mode 100644
index 0000000..201581f
Binary files /dev/null and b/docs/assets/font/material-design-icons/Material-Design-Icons.woff differ
diff --git a/docs/assets/font/material-design-icons/Material-Design-Icons.woff2 b/docs/assets/font/material-design-icons/Material-Design-Icons.woff2
new file mode 100644
index 0000000..5efd1b9
Binary files /dev/null and b/docs/assets/font/material-design-icons/Material-Design-Icons.woff2 differ
diff --git a/docs/assets/font/roboto/Roboto-Bold.eot b/docs/assets/font/roboto/Roboto-Bold.eot
new file mode 100644
index 0000000..b73776e
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Bold.eot differ
diff --git a/docs/assets/font/roboto/Roboto-Bold.ttf b/docs/assets/font/roboto/Roboto-Bold.ttf
new file mode 100644
index 0000000..68822ca
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Bold.ttf differ
diff --git a/docs/assets/font/roboto/Roboto-Bold.woff b/docs/assets/font/roboto/Roboto-Bold.woff
new file mode 100644
index 0000000..1f75afd
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Bold.woff differ
diff --git a/docs/assets/font/roboto/Roboto-Bold.woff2 b/docs/assets/font/roboto/Roboto-Bold.woff2
new file mode 100644
index 0000000..350d1c3
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Bold.woff2 differ
diff --git a/docs/assets/font/roboto/Roboto-Light.eot b/docs/assets/font/roboto/Roboto-Light.eot
new file mode 100644
index 0000000..072cdc4
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Light.eot differ
diff --git a/docs/assets/font/roboto/Roboto-Light.ttf b/docs/assets/font/roboto/Roboto-Light.ttf
new file mode 100644
index 0000000..aa45340
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Light.ttf differ
diff --git a/docs/assets/font/roboto/Roboto-Light.woff b/docs/assets/font/roboto/Roboto-Light.woff
new file mode 100644
index 0000000..3480c6c
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Light.woff differ
diff --git a/docs/assets/font/roboto/Roboto-Light.woff2 b/docs/assets/font/roboto/Roboto-Light.woff2
new file mode 100644
index 0000000..9a4d98c
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Light.woff2 differ
diff --git a/docs/assets/font/roboto/Roboto-Medium.eot b/docs/assets/font/roboto/Roboto-Medium.eot
new file mode 100644
index 0000000..f9ad995
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Medium.eot differ
diff --git a/docs/assets/font/roboto/Roboto-Medium.ttf b/docs/assets/font/roboto/Roboto-Medium.ttf
new file mode 100644
index 0000000..a3c1a1f
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Medium.ttf differ
diff --git a/docs/assets/font/roboto/Roboto-Medium.woff b/docs/assets/font/roboto/Roboto-Medium.woff
new file mode 100644
index 0000000..1186773
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Medium.woff differ
diff --git a/docs/assets/font/roboto/Roboto-Medium.woff2 b/docs/assets/font/roboto/Roboto-Medium.woff2
new file mode 100644
index 0000000..d10a592
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Medium.woff2 differ
diff --git a/docs/assets/font/roboto/Roboto-Regular.eot b/docs/assets/font/roboto/Roboto-Regular.eot
new file mode 100644
index 0000000..9b5e8e4
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Regular.eot differ
diff --git a/docs/assets/font/roboto/Roboto-Regular.ttf b/docs/assets/font/roboto/Roboto-Regular.ttf
new file mode 100644
index 0000000..0e58508
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Regular.ttf differ
diff --git a/docs/assets/font/roboto/Roboto-Regular.woff b/docs/assets/font/roboto/Roboto-Regular.woff
new file mode 100644
index 0000000..f823258
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Regular.woff differ
diff --git a/docs/assets/font/roboto/Roboto-Regular.woff2 b/docs/assets/font/roboto/Roboto-Regular.woff2
new file mode 100644
index 0000000..b7082ef
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Regular.woff2 differ
diff --git a/docs/assets/font/roboto/Roboto-Thin.eot b/docs/assets/font/roboto/Roboto-Thin.eot
new file mode 100644
index 0000000..2284a3b
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Thin.eot differ
diff --git a/docs/assets/font/roboto/Roboto-Thin.ttf b/docs/assets/font/roboto/Roboto-Thin.ttf
new file mode 100644
index 0000000..8779333
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Thin.ttf differ
diff --git a/docs/assets/font/roboto/Roboto-Thin.woff b/docs/assets/font/roboto/Roboto-Thin.woff
new file mode 100644
index 0000000..2a98c1e
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Thin.woff differ
diff --git a/docs/assets/font/roboto/Roboto-Thin.woff2 b/docs/assets/font/roboto/Roboto-Thin.woff2
new file mode 100644
index 0000000..a38025a
Binary files /dev/null and b/docs/assets/font/roboto/Roboto-Thin.woff2 differ
diff --git a/docs/assets/fonts/roboto/Roboto-Bold.eot b/docs/assets/fonts/roboto/Roboto-Bold.eot
new file mode 100644
index 0000000..b73776e
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Bold.eot differ
diff --git a/docs/assets/fonts/roboto/Roboto-Bold.ttf b/docs/assets/fonts/roboto/Roboto-Bold.ttf
new file mode 100644
index 0000000..68822ca
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Bold.ttf differ
diff --git a/docs/assets/fonts/roboto/Roboto-Bold.woff b/docs/assets/fonts/roboto/Roboto-Bold.woff
new file mode 100644
index 0000000..1f75afd
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Bold.woff differ
diff --git a/docs/assets/fonts/roboto/Roboto-Bold.woff2 b/docs/assets/fonts/roboto/Roboto-Bold.woff2
new file mode 100644
index 0000000..350d1c3
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Bold.woff2 differ
diff --git a/docs/assets/fonts/roboto/Roboto-Light.eot b/docs/assets/fonts/roboto/Roboto-Light.eot
new file mode 100644
index 0000000..072cdc4
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Light.eot differ
diff --git a/docs/assets/fonts/roboto/Roboto-Light.ttf b/docs/assets/fonts/roboto/Roboto-Light.ttf
new file mode 100644
index 0000000..aa45340
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Light.ttf differ
diff --git a/docs/assets/fonts/roboto/Roboto-Light.woff b/docs/assets/fonts/roboto/Roboto-Light.woff
new file mode 100644
index 0000000..3480c6c
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Light.woff differ
diff --git a/docs/assets/fonts/roboto/Roboto-Light.woff2 b/docs/assets/fonts/roboto/Roboto-Light.woff2
new file mode 100644
index 0000000..9a4d98c
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Light.woff2 differ
diff --git a/docs/assets/fonts/roboto/Roboto-Medium.eot b/docs/assets/fonts/roboto/Roboto-Medium.eot
new file mode 100644
index 0000000..f9ad995
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Medium.eot differ
diff --git a/docs/assets/fonts/roboto/Roboto-Medium.ttf b/docs/assets/fonts/roboto/Roboto-Medium.ttf
new file mode 100644
index 0000000..a3c1a1f
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Medium.ttf differ
diff --git a/docs/assets/fonts/roboto/Roboto-Medium.woff b/docs/assets/fonts/roboto/Roboto-Medium.woff
new file mode 100644
index 0000000..1186773
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Medium.woff differ
diff --git a/docs/assets/fonts/roboto/Roboto-Medium.woff2 b/docs/assets/fonts/roboto/Roboto-Medium.woff2
new file mode 100644
index 0000000..d10a592
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Medium.woff2 differ
diff --git a/docs/assets/fonts/roboto/Roboto-Regular.eot b/docs/assets/fonts/roboto/Roboto-Regular.eot
new file mode 100644
index 0000000..9b5e8e4
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Regular.eot differ
diff --git a/docs/assets/fonts/roboto/Roboto-Regular.ttf b/docs/assets/fonts/roboto/Roboto-Regular.ttf
new file mode 100644
index 0000000..0e58508
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Regular.ttf differ
diff --git a/docs/assets/fonts/roboto/Roboto-Regular.woff b/docs/assets/fonts/roboto/Roboto-Regular.woff
new file mode 100644
index 0000000..f823258
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Regular.woff differ
diff --git a/docs/assets/fonts/roboto/Roboto-Regular.woff2 b/docs/assets/fonts/roboto/Roboto-Regular.woff2
new file mode 100644
index 0000000..b7082ef
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Regular.woff2 differ
diff --git a/docs/assets/fonts/roboto/Roboto-Thin.eot b/docs/assets/fonts/roboto/Roboto-Thin.eot
new file mode 100644
index 0000000..2284a3b
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Thin.eot differ
diff --git a/docs/assets/fonts/roboto/Roboto-Thin.ttf b/docs/assets/fonts/roboto/Roboto-Thin.ttf
new file mode 100644
index 0000000..8779333
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Thin.ttf differ
diff --git a/docs/assets/fonts/roboto/Roboto-Thin.woff b/docs/assets/fonts/roboto/Roboto-Thin.woff
new file mode 100644
index 0000000..2a98c1e
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Thin.woff differ
diff --git a/docs/assets/fonts/roboto/Roboto-Thin.woff2 b/docs/assets/fonts/roboto/Roboto-Thin.woff2
new file mode 100644
index 0000000..a38025a
Binary files /dev/null and b/docs/assets/fonts/roboto/Roboto-Thin.woff2 differ
diff --git a/docs/assets/images/bg.jpg b/docs/assets/images/bg.jpg
new file mode 100644
index 0000000..bcaf401
Binary files /dev/null and b/docs/assets/images/bg.jpg differ
diff --git a/docs/assets/images/logo.png b/docs/assets/images/logo.png
new file mode 100644
index 0000000..358afc0
Binary files /dev/null and b/docs/assets/images/logo.png differ
diff --git a/docs/assets/js/jquery-2.1.1.min.js b/docs/assets/js/jquery-2.1.1.min.js
new file mode 100644
index 0000000..9ed2acc
--- /dev/null
+++ b/docs/assets/js/jquery-2.1.1.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML=" ",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML=" ","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML=" ",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+Math.random()}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)
+},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""," "],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>$2>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("")).appendTo(b.documentElement),b=qb[0].contentDocument,b.write(),b.close(),c=sb(a,b),qb.detach()),rb[a]=c),c}var ub=/^margin/,vb=new RegExp("^("+Q+")(?!px)[a-z%]+$","i"),wb=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)};function xb(a,b,c){var d,e,f,g,h=a.style;return c=c||wb(a),c&&(g=c.getPropertyValue(b)||c[b]),c&&(""!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),vb.test(g)&&ub.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function yb(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d=l.documentElement,e=l.createElement("div"),f=l.createElement("div");if(f.style){f.style.backgroundClip="content-box",f.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===f.style.backgroundClip,e.style.cssText="border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute",e.appendChild(f);function g(){f.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",f.innerHTML="",d.appendChild(e);var g=a.getComputedStyle(f,null);b="1%"!==g.top,c="4px"===g.width,d.removeChild(e)}a.getComputedStyle&&n.extend(k,{pixelPosition:function(){return g(),b},boxSizingReliable:function(){return null==c&&g(),c},reliableMarginRight:function(){var b,c=f.appendChild(l.createElement("div"));return c.style.cssText=f.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",f.style.width="1px",d.appendChild(e),b=!parseFloat(a.getComputedStyle(c,null).marginRight),d.removeChild(e),b}})}}(),n.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var zb=/^(none|table(?!-c[ea]).+)/,Ab=new RegExp("^("+Q+")(.*)$","i"),Bb=new RegExp("^([+-])=("+Q+")","i"),Cb={position:"absolute",visibility:"hidden",display:"block"},Db={letterSpacing:"0",fontWeight:"400"},Eb=["Webkit","O","Moz","ms"];function Fb(a,b){if(b in a)return b;var c=b[0].toUpperCase()+b.slice(1),d=b,e=Eb.length;while(e--)if(b=Eb[e]+c,b in a)return b;return d}function Gb(a,b,c){var d=Ab.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Hb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+R[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+R[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+R[f]+"Width",!0,e))):(g+=n.css(a,"padding"+R[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+R[f]+"Width",!0,e)));return g}function Ib(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=wb(a),g="border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=xb(a,b,f),(0>e||null==e)&&(e=a.style[b]),vb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Hb(a,b,c||(g?"border":"content"),d,f)+"px"}function Jb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=L.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&S(d)&&(f[g]=L.access(d,"olddisplay",tb(d.nodeName)))):(e=S(d),"none"===c&&e||L.set(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=xb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;return b=n.cssProps[h]||(n.cssProps[h]=Fb(i,h)),g=n.cssHooks[b]||n.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=Bb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(n.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||n.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Fb(a.style,h)),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=xb(a,b,d)),"normal"===e&&b in Db&&(e=Db[b]),""===c||c?(f=parseFloat(e),c===!0||n.isNumeric(f)?f||0:e):e}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?zb.test(n.css(a,"display"))&&0===a.offsetWidth?n.swap(a,Cb,function(){return Ib(a,b,d)}):Ib(a,b,d):void 0},set:function(a,c,d){var e=d&&wb(a);return Gb(a,c,d?Hb(a,b,d,"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),n.cssHooks.marginRight=yb(k.reliableMarginRight,function(a,b){return b?n.swap(a,{display:"inline-block"},xb,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+R[d]+b]=f[d]||f[d-2]||f[0];return e}},ub.test(a)||(n.cssHooks[a+b].set=Gb)}),n.fn.extend({css:function(a,b){return J(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=wb(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return Jb(this,!0)},hide:function(){return Jb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){S(this)?n(this).show():n(this).hide()})}});function Kb(a,b,c,d,e){return new Kb.prototype.init(a,b,c,d,e)}n.Tween=Kb,Kb.prototype={constructor:Kb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=Kb.propHooks[this.prop];return a&&a.get?a.get(this):Kb.propHooks._default.get(this)},run:function(a){var b,c=Kb.propHooks[this.prop];return this.pos=b=this.options.duration?n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Kb.propHooks._default.set(this),this}},Kb.prototype.init.prototype=Kb.prototype,Kb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[n.cssProps[a.prop]]||n.cssHooks[a.prop])?n.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Kb.propHooks.scrollTop=Kb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},n.fx=Kb.prototype.init,n.fx.step={};var Lb,Mb,Nb=/^(?:toggle|show|hide)$/,Ob=new RegExp("^(?:([+-])=|)("+Q+")([a-z%]*)$","i"),Pb=/queueHooks$/,Qb=[Vb],Rb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=Ob.exec(b),f=e&&e[3]||(n.cssNumber[a]?"":"px"),g=(n.cssNumber[a]||"px"!==f&&+d)&&Ob.exec(n.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,n.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function Sb(){return setTimeout(function(){Lb=void 0}),Lb=n.now()}function Tb(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=R[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ub(a,b,c){for(var d,e=(Rb[b]||[]).concat(Rb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Vb(a,b,c){var d,e,f,g,h,i,j,k,l=this,m={},o=a.style,p=a.nodeType&&S(a),q=L.get(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,l.always(function(){l.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=n.css(a,"display"),k="none"===j?L.get(a,"olddisplay")||tb(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(o.display="inline-block")),c.overflow&&(o.overflow="hidden",l.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Nb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}m[d]=q&&q[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(m))"inline"===("none"===j?tb(a.nodeName):j)&&(o.display=j);else{q?"hidden"in q&&(p=q.hidden):q=L.access(a,"fxshow",{}),f&&(q.hidden=!p),p?n(a).show():l.done(function(){n(a).hide()}),l.done(function(){var b;L.remove(a,"fxshow");for(b in m)n.style(a,b,m[b])});for(d in m)g=Ub(p?q[d]:0,d,l),d in q||(q[d]=g.start,p&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function Wb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function Xb(a,b,c){var d,e,f=0,g=Qb.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Lb||Sb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:Lb||Sb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(Wb(k,j.opts.specialEasing);g>f;f++)if(d=Qb[f].call(j,a,k,j.opts))return d;return n.map(k,Ub,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(Xb,{tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],Rb[c]=Rb[c]||[],Rb[c].unshift(b)},prefilter:function(a,b){b?Qb.unshift(a):Qb.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(S).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=Xb(this,n.extend({},a),f);(e||L.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=L.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Pb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=L.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Tb(b,!0),a,d,e)}}),n.each({slideDown:Tb("show"),slideUp:Tb("hide"),slideToggle:Tb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=0,c=n.timers;for(Lb=n.now();b1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?Zb:Yb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b))
+},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$b[b]=function(a,b,d){var e,f;return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cc=n.now(),dc=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&n.error("Invalid XML: "+a),b};var ec,fc,gc=/#.*$/,hc=/([?&])_=[^&]*/,ic=/^(.*?):[ \t]*([^\r\n]*)$/gm,jc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,kc=/^(?:GET|HEAD)$/,lc=/^\/\//,mc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,nc={},oc={},pc="*/".concat("*");try{fc=location.href}catch(qc){fc=l.createElement("a"),fc.href="",fc=fc.href}ec=mc.exec(fc.toLowerCase())||[];function rc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function sc(a,b,c,d){var e={},f=a===oc;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function tc(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function uc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function vc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:fc,type:"GET",isLocal:jc.test(ec[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":pc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?tc(tc(a,n.ajaxSettings),b):tc(n.ajaxSettings,a)},ajaxPrefilter:rc(nc),ajaxTransport:rc(oc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=ic.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||fc)+"").replace(gc,"").replace(lc,ec[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=mc.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===ec[1]&&h[2]===ec[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(ec[3]||("http:"===ec[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),sc(nc,k,b,v),2===t)return v;i=k.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!kc.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(dc.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=hc.test(d)?d.replace(hc,"$1_="+cc++):d+(dc.test(d)?"&":"?")+"_="+cc++)),k.ifModified&&(n.lastModified[d]&&v.setRequestHeader("If-Modified-Since",n.lastModified[d]),n.etag[d]&&v.setRequestHeader("If-None-Match",n.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+pc+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=sc(oc,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=uc(k,v,f)),u=vc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(n.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var wc=/%20/g,xc=/\[\]$/,yc=/\r?\n/g,zc=/^(?:submit|button|image|reset|file)$/i,Ac=/^(?:input|select|textarea|keygen)/i;function Bc(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||xc.test(a)?d(a,e):Bc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Bc(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Bc(c,a[c],b,e);return d.join("&").replace(wc,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&Ac.test(this.nodeName)&&!zc.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(yc,"\r\n")}}):{name:b.name,value:c.replace(yc,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Cc=0,Dc={},Ec={0:200,1223:204},Fc=n.ajaxSettings.xhr();a.ActiveXObject&&n(a).on("unload",function(){for(var a in Dc)Dc[a]()}),k.cors=!!Fc&&"withCredentials"in Fc,k.ajax=Fc=!!Fc,n.ajaxTransport(function(a){var b;return k.cors||Fc&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Cc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Dc[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Ec[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Dc[g]=b("abort");try{f.send(a.hasContent&&a.data||null)}catch(h){if(b)throw h}},abort:function(){b&&b()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=n("
\ No newline at end of file
diff --git a/plugin_info/configuration.php b/plugin_info/configuration.php
new file mode 100644
index 0000000..0acc3b9
--- /dev/null
+++ b/plugin_info/configuration.php
@@ -0,0 +1,57 @@
+.
+ */
+
+require_once dirname(__FILE__) . '/../../../core/php/core.inc.php';
+include_file('core', 'authentification', 'php');
+if (!isConnect('admin')) {
+ throw new Exception('{{401 - Accès non autorisé}}');
+}
+?>
+
+
diff --git a/plugin_info/googlecast_icon.png b/plugin_info/googlecast_icon.png
new file mode 100644
index 0000000..41a6720
Binary files /dev/null and b/plugin_info/googlecast_icon.png differ
diff --git a/plugin_info/info.json b/plugin_info/info.json
new file mode 100644
index 0000000..da60886
--- /dev/null
+++ b/plugin_info/info.json
@@ -0,0 +1,13 @@
+{
+ "id" : "googlecast",
+ "name" : "Google Cast",
+ "description" : "Plugin de gestion des équipements Google Cast",
+ "licence" : "AGPL",
+ "author" : "guirem",
+ "hasOwnDeamon" : true,
+ "hasDependency" : true,
+ "require" : "3.0",
+ "category" : "multimedia",
+ "changelog" : "https://jeedom.github.io/plugin-googlecast/#language#/changelog",
+ "documentation" : "https://jeedom.github.io/plugin-googlecast/#language#/"
+}
diff --git a/plugin_info/install.php b/plugin_info/install.php
new file mode 100644
index 0000000..98f2a4a
--- /dev/null
+++ b/plugin_info/install.php
@@ -0,0 +1,57 @@
+.
+ */
+
+require_once dirname(__FILE__) . '/../../../core/php/core.inc.php';
+
+function googlecast_update() {
+ linkTemplate('cmd.info.string.googlecast_playing.html');
+
+ foreach (googlecast::byType('googlecast') as $googlecast) {
+ try {
+ $googlecast->save();
+ } catch (Exception $e) {}
+ }
+}
+
+function googlecast_install() {
+ linkTemplate('cmd.info.string.googlecast_playing.html');
+}
+
+function googlecast_remove() {
+ unlinkTemplate('cmd.info.string.googlecast_playing.html');
+}
+
+function linkTemplate($templateFilename) {
+ log::add('googlecast','info',"Création du lien sur template " . $templateFilename);
+ $pathSrc = dirname(__FILE__) . '/../core/template/dashboard/'.$templateFilename;
+ $pathDest = dirname(__FILE__) . '/../../../core/template/dashboard/'.$templateFilename;
+
+ if (!file_exists($pathDest)) {
+ shell_exec('ln -s '.$pathSrc. ' '. $pathDest);
+ }
+}
+
+function unlinkTemplate($templateFilename) {
+ log::add('googlecast','info',"Suppression du lieu du template " . $templateFilename);
+ $path = dirname(__FILE__) . '/../../../core/template/dashboard/'.$templateFilename;
+
+ if (file_exists($path)) {
+ unlink($path);
+ }
+}
+?>
diff --git a/resources/globals.py b/resources/globals.py
new file mode 100644
index 0000000..49e6a10
--- /dev/null
+++ b/resources/globals.py
@@ -0,0 +1,48 @@
+import time
+
+JEEDOM_COM = ''
+KNOWN_DEVICES = {}
+NOWPLAYING_DEVICES = {}
+GCAST_DEVICES = {}
+
+NOWPLAYING_TIMEOUT = 15*60 # 15 minutes
+NOWPLAYING_FREQUENCY = 15 # 30 seconds
+NOWPLAYING_LAST = int(time.time())
+
+LEARN_BEGIN = int(time.time())
+LEARN_MODE = False # is learn mode ?
+LEARN_TIMEOUT = 60
+
+HEARTBEAT_FREQUENCY = 300 # 5 minutes
+LAST_BEAT = int(time.time())
+
+READ_FREQUENCY = 60 # in seconds
+
+SCAN_FREQUENCY = 60 # in seconds
+SCAN_PENDING = False # is scanner running?
+SCAN_LAST = 0 # when last started
+
+EVENTLISTENER_FILTERDELAY = 1 # in seconds, filter out multiple events that trigger in the same time
+EVENTLISTENER_LASTEVENT = 0
+EVENTLISTENER_NBTRIES = 10
+
+LOSTDEVICE_RESENDNOTIFDELAY = 60*5 # not used yet
+
+IFACE_DEVICE = 0
+
+log_level = "debug"
+pidfile = '/tmp/googlecast.pid'
+apikey = ''
+callback = ''
+cycle = 0.3
+daemonname=''
+socketport=55012
+sockethost=''
+device=''
+
+# dev
+callback = 'http://127.0.0.1:80/plugins/googlecast/core/php/googlecast.api.php'
+apikey = '4UMYbYhLSDhxrZ8dPlgFkOQLMZ9ndlEe'
+sockethost = '127.0.0.1'
+#KNOWN_DEVICES['d2fd3db1-bd0f-fe4c-d8dd-ce8c44033b44'] = {}
+#REALTIME_DEVICES['d2fd3db1-bd0f-fe4c-d8dd-ce8c44033b44'] = {}
diff --git a/resources/googlecast.py b/resources/googlecast.py
new file mode 100644
index 0000000..10dcd7c
--- /dev/null
+++ b/resources/googlecast.py
@@ -0,0 +1,775 @@
+# This file is part of Jeedom.
+#
+# Jeedom is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Jeedom is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Jeedom. If not, see .
+
+import subprocess
+import os,re
+import logging
+import sys
+import argparse
+import time
+from datetime import datetime
+import signal
+import json
+import traceback
+import select
+
+import globals
+from threading import Timer
+import _thread as thread
+
+try:
+ import pychromecast.pychromecast as pychromecast
+ import pychromecast.pychromecast.controllers.dashcast as dashcast
+ import pychromecast.pychromecast.controllers.youtube as youtube
+ import pychromecast.pychromecast.controllers.plex as plex
+ import pychromecast.pychromecast.controllers.spotify as Spotify
+except ImportError:
+ print("Error: importing pychromecast module")
+ sys.exit(1)
+
+try:
+ from jeedom.jeedom import *
+except ImportError:
+ print("Error: importing module from jeedom folder")
+ sys.exit(1)
+
+
+class JeedomChromeCast :
+ def __init__(self, gcast):
+ self.gcast = gcast
+ self.gcast.media_controller.register_status_listener(self)
+ self.gcast.register_status_listener(self)
+ self.previous_status = {"uuid" : self.uuid, "online" : False}
+ self.gcast.register_connection_listener(self)
+ self.now_playing = False
+ self.online = True
+ if self.gcast.device.cast_type != 'cast' and self.friendly_name not in pychromecast.IGNORE_CEC:
+ pychromecast.IGNORE_CEC.append(self.gcast.device.friendly_name)
+
+ @property
+ def device(self):
+ return self.gcast.device
+
+ @property
+ def uuid(self):
+ return str(self.gcast.device.uuid)
+
+ def friendly_name(self):
+ return self.gcast.device.friendly_name
+
+ def startNowPlaying(self):
+ if self.now_playing == False and self.online == True:
+ logging.debug("JEEDOMCHROMECAST------ Starting monitoring of " + self.uuid)
+ self.now_playing = True
+ thread.start_new_thread(self.thread_nowlaying, ("nowPlayingTHread",))
+
+ def stopNowPlaying(self):
+ logging.debug("JEEDOMCHROMECAST------ Stopping monitoring of " + self.uuid)
+ self.now_playing = False
+
+ def new_cast_status(self, new_status):
+ #logging.debug("JEEDOMCHROMECAST------ Status " + str(new_status))
+ self._internal_refresh_status(False)
+
+ def new_connection_status(self, new_status):
+ logging.debug("JEEDOMCHROMECAST------ Connection " + str(new_status.status))
+ if new_status.status == "DISCONNECTED" :
+ self.disconnect()
+ else :
+ self.online = True
+
+ def sendDeviceStatus(self, _force=True):
+ try :
+ self.gcast.media_controller.update_status()
+ time.sleep(0.2)
+ except Exception :
+ pass
+ self._internal_refresh_status(_force)
+
+ def disconnect(self):
+ #if self.online == True :
+ self.online = False
+ if self.now_playing :
+ self._internal_send_now_playing()
+ time.sleep(1)
+ self.now_playing = False
+ if self.uuid in globals.GCAST_DEVICES :
+ del globals.GCAST_DEVICES[self.uuid]
+ self.gcast.disconnect()
+
+ def sendDeviceStatusIfNew(self):
+ self.sendDeviceStatus(False)
+
+ def loadControler(self, controler, _quit_app_before=True, _force_register=False):
+ if self.gcast.socket_client :
+ if controler.namespace in self.gcast.socket_client._handlers and not _force_register :
+ logging.debug("JEEDOMCHROMECAST------ Loading controller from memory " + str(controler.namespace))
+ return self.gcast.socket_client._handlers[controler.namespace]
+ else :
+ logging.debug("JEEDOMCHROMECAST------ Initiating controler " + str(controler.namespace))
+ self.gcast.register_handler(controler)
+ time.sleep(1)
+ if not self.gcast.is_idle and _quit_app_before:
+ self.gcast.quit_app()
+ time.sleep(3)
+ return controler
+
+ def _internal_refresh_status(self,_force = False):
+ uuid = str(self.device.uuid)
+ status = self._internal_get_status()
+ if _force or self._internal_status_different(status) :
+ logging.debug("Detected changes in status of " +self.device.friendly_name)
+ globals.KNOWN_DEVICES[uuid]['status'] = status
+ self.previous_status = status
+ globals.KNOWN_DEVICES[uuid]['online'] = self.online
+ globals.JEEDOM_COM.add_changes('devices::'+uuid,globals.KNOWN_DEVICES[uuid])
+ globals.KNOWN_DEVICES[uuid]['lastSent'] = int(time.time())
+
+ def _internal_get_status(self):
+ if self.gcast.status!=None :
+ self.online = True
+ uuid = self.uuid
+ status = {
+ "uuid" : uuid,
+ "friendly_name" : self.gcast.device.friendly_name,
+ "is_active_input" : True if self.gcast.status.is_active_input else False,
+ "is_stand_by" : True if self.gcast.status.is_stand_by else False,
+ "volume_level" : int(self.gcast.status.volume_level*100),
+ "volume_muted" : self.gcast.status.volume_muted,
+ "app_id" : self.gcast.status.app_id,
+ "display_name" : self.gcast.status.display_name,
+ "status_text" : self.gcast.status.status_text,
+ "is_busy" : not self.gcast.is_idle,
+ }
+ return status
+ else :
+ self.online = False
+ return {
+ "uuid" : self.uuid,
+ "friendly_name" : "", "is_stand_by" : False, "is_active_input" : False,
+ "app_id" : "", "display_name" : "", "status_text" : "",
+ "status_text" : "", "is_busy" : False,
+ }
+
+
+ def _internal_status_different(self, new_status):
+ prev_status = self.previous_status
+ if len(prev_status) != len(new_status) or len(prev_status)==2 or 'volume_level' not in new_status :
+ return True
+ if prev_status['volume_level'] != new_status['volume_level'] :
+ return True
+ if prev_status['app_id'] != new_status['app_id'] :
+ return True
+ if prev_status['is_busy'] != new_status['is_busy'] :
+ return True
+ if prev_status['volume_level'] != new_status['volume_level'] :
+ return True
+ if prev_status['volume_muted'] != new_status['volume_muted'] :
+ return True
+ if prev_status['is_stand_by'] != new_status['is_stand_by'] :
+ return True
+ if prev_status['status_text'] != new_status['status_text'] :
+ return True
+ return False
+
+ def getDefinition(self):
+ uuid = str(self.gcast.device.uuid)
+ status = {
+ "friendly_name" : self.gcast.device.friendly_name,
+ "model_name" : self.gcast.device.model_name,
+ "manufacturer" : self.gcast.device.manufacturer,
+ "cast_type" : self.gcast.device.cast_type,
+ "uri" : self.gcast.uri
+ }
+ return status
+
+ def getStatus(self):
+ return self._internal_get_status()
+
+
+ def thread_nowlaying(self, name):
+ logging.debug(" JEEDOMCHROMECAST------Starting NowPlaying thread for " + self.uuid)
+ delay = 0
+ firstTime = True
+ while self.now_playing :
+ if delay >= globals.NOWPLAYING_FREQUENCY or firstTime :
+ try :
+ self.gcast.media_controller.update_status(callback_function_param=self._internal_send_now_playing)
+ except Exception :
+ self._internal_send_now_playing()
+ pass
+ firstTime = False
+ delay = 0
+ delay = delay + 1
+ time.sleep(1)
+
+ logging.debug(" JEEDOMCHROMECAST------Closing NowPlaying thread for " + self.uuid)
+
+ def _internal_send_now_playing(self, message=None):
+ #logging.debug(" JEEDOMCHROMECAST------crap message " + str(crap))
+ uuid = self.uuid
+ if self.gcast.status:
+ playStatus = self.gcast.media_controller.status
+ if len(playStatus.images) > 0 :
+ img = str(playStatus.images[0].url)
+ else:
+ img = None
+ data = {
+ "uuid" : uuid,
+ "online" : True,
+ "friendly_name" : self.gcast.device.friendly_name,
+ "is_active_input" : True if self.gcast.status.is_active_input else False,
+ "is_stand_by" : True if self.gcast.status.is_stand_by else False,
+ "volume_level" : int(self.gcast.status.volume_level*100), #"{0:.2f}".format(cast.status.volume_level),
+ "volume_muted" : self.gcast.status.volume_muted,
+ "app_id" : self.gcast.status.app_id,
+ "display_name" : self.gcast.status.display_name,
+ "status_text" : self.gcast.status.status_text,
+ "is_busy" : not self.gcast.is_idle,
+ "title" : playStatus.title,
+ "album_artist" : playStatus.album_artist,
+ "metadata_type" : playStatus.metadata_type,
+ "album_name" : playStatus.album_name,
+ "current_time" : '{0:.0f}'.format(playStatus.current_time),
+ "artist" : playStatus.artist,
+ 'series_title': playStatus.series_title,
+ 'season': playStatus.season,
+ 'episode': playStatus.episode,
+ "image" : img,
+ "stream_type" : playStatus.stream_type,
+ "track" : playStatus.track,
+ "player_state" : playStatus.player_state,
+ "supported_media_commands" : playStatus.supported_media_commands,
+ "supports_pause" : playStatus.supports_pause,
+ 'duration': playStatus.duration, #'{0:.0f}'.format(playStatus.duration),
+ 'content_type': playStatus.content_type,
+ 'idle_reason': playStatus.idle_reason
+ }
+ globals.JEEDOM_COM.send_change_immediate({'uuid' : uuid, 'nowplaying':data});
+
+ else :
+ data = {
+ "uuid" : uuid,
+ "online" : False, "friendly_name" : "",
+ "is_active_input" : False, "is_stand_by" : False,
+ "app_id" : "", "display_name" : "", "status_text" : "",
+ "is_busy" : False, "title" : "",
+ "album_artist" : "","metadata_type" : "",
+ "album_name" : "", "current_time" : 0,
+ "artist" : "", "image" : None,
+ 'series_title': "", 'season': "", 'episode': "",
+ "stream_type" : "", "track" : "",
+ "player_state" : "","supported_media_commands" : 0,
+ "supports_pause" : "", 'duration': 0,
+ 'content_type': "", 'idle_reason': ""
+ }
+ globals.JEEDOM_COM.send_change_immediate({'uuid' : uuid, 'nowplaying':data});
+
+
+# -------------------------------
+# main method to manage actions
+# -------------------------------
+def action_handler(message):
+ #name = message['command']['name']
+ rootcmd = message['cmd']
+ uuid = message['command']['device']['uuid']
+
+ cmd = 'NONE'
+ if 'cmd' in message['command'] :
+ cmd = message['command']['cmd']
+ app = 'media'
+ if 'app' in message['command'] :
+ app = message['command']['app']
+ value = None
+ if 'value' in message['command'] :
+ value = message['command']['value']
+
+ needSendStatus = True
+ logging.debug("ACTION------ " + rootcmd + " - " + cmd + ' - ' + uuid + ' - ' + str(value)+ ' - ' + app)
+ if uuid in globals.KNOWN_DEVICES and uuid in globals.GCAST_DEVICES and rootcmd == "action":
+
+ gcast = globals.GCAST_DEVICES[uuid].gcast
+ if cmd == 'refresh':
+ logging.debug("ACTION------Refresh action")
+ elif cmd == 'reboot':
+ logging.debug("ACTION------Reboot action")
+ gcast.reboot()
+ time.sleep(5)
+ elif cmd == 'volume_up':
+ logging.debug("ACTION------Volumme up action")
+ gcast.volume_up(value if value!=None else 0.1)
+ elif cmd == 'volume_down':
+ logging.debug("ACTION------Volume down action")
+ gcast.volume_down(value if value!=None else 0.1)
+ elif cmd == 'volume_set':
+ logging.debug("ACTION------Volume set action")
+ gcast.set_volume(int(value)/100)
+ elif cmd == 'play':
+ logging.debug("ACTION------Play action")
+ gcast.media_controller.play()
+ elif cmd == 'stop':
+ logging.debug("ACTION------Stop action")
+ gcast.media_controller.stop()
+ elif cmd == 'rewind':
+ logging.debug("ACTION------Rewind action")
+ gcast.media_controller.rewind()
+ elif cmd == 'skip':
+ logging.debug("ACTION------Skip action")
+ gcast.media_controller.skip()
+ elif cmd == 'seek':
+ logging.debug("ACTION------Seek action")
+ gcast.media_controller.seek(0 if value==None else value)
+ elif cmd == 'start_app':
+ logging.debug("ACTION------Stop action")
+ gcast.start_app('' if value==None else value)
+ elif cmd == 'quit_app':
+ logging.debug("ACTION------Stop action")
+ gcast.quit_app()
+ elif cmd == 'pause':
+ logging.debug("ACTION------Stop action")
+ gcast.media_controller.pause()
+ elif cmd == 'mute_on':
+ logging.debug("ACTION------Mute on action")
+ gcast.set_volume_muted(True)
+ elif cmd == 'mute_off':
+ logging.debug("ACTION------Mute off action")
+ gcast.set_volume_muted(False)
+ elif app != None:
+ logging.debug("ACTION------Plyaing action " + cmd + ' for application ' + app)
+ try:
+ quit_app_before=True
+ if 'quit_app_before' in message['command'] :
+ quit_app_before = True if message['command']['quit_app_before'] else False
+ force_register=False
+ if 'force_register' in message['command'] :
+ force_register = True if message['command']['force_register'] else False
+
+ if app == 'web': # app=web|cmd=load_url|value=https://news.google.com,True,5
+ force_register=True
+ possibleCmd = ['load_url']
+ if cmd in possibleCmd :
+ player = dashcast.DashCastController()
+ player = globals.GCAST_DEVICES[uuid].loadControler(player, quit_app_before, force_register)
+ eval( 'player.' + cmd + '('+ gcast_prepareAppParam(value) +')' )
+ elif app == 'youtube': # app=youtube|cmd=play_video|value=fra4QBLF3GU
+ if gcast.device.cast_type == 'cast' :
+ possibleCmd = ['play_video', 'add_to_queue', 'update_screen_id', 'clear_playlist']
+ if cmd in possibleCmd :
+ player = youtube.YouTubeController()
+ player = globals.GCAST_DEVICES[uuid].loadControler(player, quit_app_before, force_register)
+ eval( 'player.' + cmd + '('+ gcast_prepareAppParam(value) +')' )
+ else :
+ logging.error("ACTION------ YouTube not availble on Chromecast Audio")
+ elif app == 'spotify': # app=spotify|cmd=launch_app|token=XXXXXX
+ possibleCmd = ['launch_app']
+ if cmd in possibleCmd :
+ if 'token' not in message['command'] :
+ logging.error("ACTION------ Token missing for Spotify")
+ else :
+ player = spotify.SpotifyController(message['command']['token'])
+ player = globals.GCAST_DEVICES[uuid].loadControler(player, quit_app_before, force_register)
+ player.launch_app()
+ elif app == 'backdrop': # also called backdrop
+ if gcast.device.cast_type == 'cast' :
+ gcast.start_app('E8C28D3C')
+ else :
+ logging.error("ACTION------ Backdrop not availble on Chromecast Audio")
+ elif app == 'plex': # app=plex|cmd=pause
+ quit_app_before=False
+ possibleCmd = ['play', 'stop', 'pause']
+ if cmd in possibleCmd :
+ player = plex.PlexController()
+ player = globals.GCAST_DEVICES[uuid].loadControler(player, quit_app_before, force_register)
+ time.sleep(1)
+ eval( 'player.' + cmd + '('+ gcast_prepareAppParam(value) +')' )
+ else : # media # app=media|cmd=play_media|value=http://bit.ly/2JzYtfX,video/mp4,Mon film
+ possibleCmd = ['play', 'stop', 'pause', 'play_media']
+ eval( 'gcast.media_controller.' + cmd + '('+ gcast_prepareAppParam(value) +')' )
+ except Exception as e:
+ logging.error("ACTION------Error while playing "+app+" : %s" % str(e))
+
+ else:
+ logging.debug("ACTION------NOT IMPLEMENTED : " + cmd)
+
+ if needSendStatus :
+ globals.GCAST_DEVICES[uuid].sendDeviceStatus()
+ return
+
+
+def gcast_prepareAppParam(params):
+ if params is None or params == '':
+ return ''
+ ret = ''
+ s = params.split(",")
+ for p in s :
+ p = p.strip()
+ s2 = p.split("=")
+ prefix = ''
+ if len(s2)==2 :
+ prefix = s2[0].strip() + '='
+ p = s2[1].strip()
+
+ if p.isnumeric() :
+ ret = ret + ',' + prefix + p
+ elif p == 'True' or p == 'False' or p == 'None' :
+ ret = ret + ',' + prefix + p
+ else :
+ if p.startswith( "'" ) : # if starts already with simple quote
+ ret = ret + ',' + prefix + p
+ else :
+ ret = ret + ',' + prefix + '"'+ p +'"' # else add quotes
+ return ret[1:]
+
+
+def read_device(name):
+ while 1:
+ now = datetime.datetime.utcnow()
+ try:
+ for uuid in list(globals.KNOWN_DEVICES):
+ if (int(time.time())-globals.KNOWN_DEVICES[uuid]['lastSent']) > globals.READ_FREQUENCY:
+ globals.JEEDOM_COM.add_changes('devices::'+uuid, globals.KNOWN_DEVICES[uuid])
+ globals.KNOWN_DEVICES[uuid]['lastSent'] = int(time.time())
+ except Exception as e:
+ logging.error("READER------Exception on read device : %s" % str(e))
+ time.sleep(10)
+
+
+def listen():
+ jeedom_socket.open()
+ logging.info("GLOBAL------Start listening...")
+ logging.info("GLOBAL------Socket started...")
+ thread.start_new_thread( read_socket, ('socket',))
+ logging.debug('GLOBAL------Heartbeat Thread Launched')
+ thread.start_new_thread( heartbeat_handler, (19,))
+ globals.JEEDOM_COM.send_change_immediate({'started' : 1,'source' : globals.daemonname});
+
+ try:
+ while 1:
+ try:
+
+ if not globals.SCAN_PENDING and globals.LEARN_MODE :
+ thread.start_new_thread( scanner, ('scan learn',))
+
+ if not globals.SCAN_PENDING and (int(time.time()) - globals.SCAN_LAST) > globals.SCAN_FREQUENCY :
+ thread.start_new_thread( scanner, ('scanner',))
+
+ while globals.SCAN_PENDING:
+ time.sleep(1)
+
+ if globals.LEARN_MODE :
+ time.sleep(0.2)
+ else :
+ time.sleep(3)
+
+ except Exception as e:
+ logging.warning("GLOBAL------Exception on scanner")
+
+ except KeyboardInterrupt:
+ logging.error("GLOBAL------KeyboardInterrupt, shutdown")
+ shutdown()
+
+
+def read_socket(name):
+ while 1:
+ try:
+ global JEEDOM_SOCKET_MESSAGE
+ if not JEEDOM_SOCKET_MESSAGE.empty():
+ logging.debug("SOCKET-READ------Message received in socket JEEDOM_SOCKET_MESSAGE")
+ #message = json.loads(jeedom_utils.stripped(JEEDOM_SOCKET_MESSAGE.get()))
+ message = json.loads(JEEDOM_SOCKET_MESSAGE.get())
+ if message['apikey'] != globals.apikey:
+ logging.error("SOCKET-READ------Invalid apikey from socket : " + str(message))
+ return
+ logging.debug('SOCKET-READ------Received command from jeedom : '+str(message['cmd']))
+ if message['cmd'] == 'add':
+ logging.debug('SOCKET-READ------Add device : '+str(message['device']))
+ if 'uuid' in message['device']:
+ uuid = message['device']['uuid']
+ if uuid not in globals.KNOWN_DEVICES :
+ globals.KNOWN_DEVICES[uuid] = {
+ 'uuid': uuid, 'status': {},
+ 'friendly_name': message['device']['name'],
+ 'lastOnline':0, 'online':False,
+ 'lastSent': 0, 'lastOfflineSent': 0
+ }
+ globals.SCAN_LAST = 0
+ elif message['cmd'] == 'remove':
+ logging.debug('SOCKET-READ------Remove device : '+str(message['device']))
+ if 'uuid' in message['device']:
+ uuid = message['device']['uuid']
+ if uuid in globals.KNOWN_DEVICES :
+ del globals.KNOWN_DEVICES[uuid]
+ if uuid in globals.GCAST_DEVICES :
+ globals.GCAST_DEVICES[uuid].disconnect()
+ del globals.GCAST_DEVICES[uuid]
+ if uuid in globals.NOWPLAYING_DEVICES :
+ del globals.NOWPLAYING_DEVICES[uuid]
+ globals.SCAN_LAST = 0
+ elif message['cmd'] == 'nowplaying':
+ if 'uuid' in message:
+ uuid = message['uuid']
+ globals.NOWPLAYING_DEVICES[uuid] = int(time.time())
+ if uuid in globals.GCAST_DEVICES :
+ logging.debug('SOCKET-READ------Now playing activated for '+uuid)
+ globals.GCAST_DEVICES[uuid].startNowPlaying()
+ else :
+ logging.debug('SOCKET-READ------Now playing for ' +uuid+ ' not activated because is offline')
+ elif message['cmd'] == 'learnin':
+ logging.debug('SOCKET-READ------Enter in learn mode')
+ globals.LEARN_MODE = True
+ globals.LEARN_BEGIN = int(time.time())
+ globals.JEEDOM_COM.send_change_immediate({'learn_mode' : 1,'source' : globals.daemonname});
+ elif message['cmd'] == 'learnout':
+ logging.debug('SOCKET-READ------Leave learn mode')
+ globals.LEARN_MODE = False
+ globals.JEEDOM_COM.send_change_immediate({'learn_mode' : 0,'source' : globals.daemonname});
+ elif message['cmd'] == 'refresh':
+ logging.debug('SOCKET-READ------Attempt a refresh on a device')
+ uuid = message['device']['uuid']
+ if uuid in globals.GCAST_DEVICES :
+ globals.GCAST_DEVICES[uuid].sendDeviceStatus()
+ thread.start_new_thread( read_device, ('action',))
+ elif message['cmd'] in ['action']:
+ logging.debug('SOCKET-READ------Attempt an action on a device')
+ thread.start_new_thread( action_handler, (message,))
+ logging.debug('SOCKET-READ------Action Thread Launched')
+ elif message['cmd'] == 'logdebug':
+ logging.info('SOCKET-READ------Passage du demon en mode debug force')
+ log = logging.getLogger()
+ for hdlr in log.handlers[:]:
+ log.removeHandler(hdlr)
+ jeedom_utils.set_log_level('debug')
+ logging.debug('SOCKET-READ------<----- La preuve ;)')
+ elif message['cmd'] == 'lognormal':
+ logging.info('SOCKET-READ------Passage du demon en mode de log initial')
+ log = logging.getLogger()
+ for hdlr in log.handlers[:]:
+ log.removeHandler(hdlr)
+ jeedom_utils.set_log_level(globals.log_level)
+ elif message['cmd'] == 'stop':
+ logging.info('SOCKET-READ------Arret du demon sur demande socket')
+ globals.JEEDOM_COM.send_change_immediate({'learn_mode' : 0,'source' : globals.daemonname});
+ time.sleep(2)
+ shutdown()
+ except Exception as e:
+ logging.error("SOCKET-READ------Exception on socket : %s" % str(e))
+ time.sleep(0.3)
+
+
+def scanner(name):
+ try:
+ logging.debug("SCANNER------ Start scanning...")
+ globals.SCAN_PENDING = True
+ DISCOVER_DEVICES = {}
+
+ scanForced = False
+ for known in globals.KNOWN_DEVICES :
+ if known not in globals.GCAST_DEVICES :
+ scanForced = scanForced or True
+
+ if scanForced==True or globals.LEARN_MODE==True:
+ logging.debug("SCANNER------ Looking for chromecasts on network...")
+ casts = pychromecast.get_chromecasts(tries=1, retry_wait=2, timeout=10)
+ else :
+ logging.debug("SCANNER------ No need to scan, everything is there")
+ casts = list(globals.GCAST_DEVICES.values())
+
+ for cast in casts :
+ uuid = str(cast.device.uuid)
+ DISCOVER_DEVICES[uuid] = {'friendly_name':cast.device.friendly_name, 'uuid': uuid, 'lastScan': int(time.time()) }
+
+ # starting event thread
+ if uuid in globals.KNOWN_DEVICES :
+ globals.KNOWN_DEVICES[uuid]['uuid'] = uuid
+ globals.KNOWN_DEVICES[uuid]['online'] = True
+ globals.KNOWN_DEVICES[uuid]['lastScan'] = int(time.time())
+ globals.KNOWN_DEVICES[uuid]["lastOnline"] = int(time.time())
+
+ if uuid not in globals.GCAST_DEVICES :
+ logging.info("SCANNER------ Detected chromecast : " + cast.device.friendly_name)
+ globals.GCAST_DEVICES[uuid] = JeedomChromeCast(cast)
+
+ if uuid in globals.NOWPLAYING_DEVICES :
+ if (int(time.time())-globals.NOWPLAYING_DEVICES[uuid]) > globals.NOWPLAYING_TIMEOUT :
+ del globals.NOWPLAYING_DEVICES[uuid]
+ globals.GCAST_DEVICES[uuid].stopNowPlaying()
+ else :
+ globals.GCAST_DEVICES[uuid].startNowPlaying()
+
+ globals.GCAST_DEVICES[uuid].sendDeviceStatusIfNew()
+
+ else :
+ if globals.LEARN_MODE :
+ data = {}
+ jcast = JeedomChromeCast(cast)
+ data = {'friendly_name':cast.device.friendly_name, 'uuid': uuid, 'lastScan': int(time.time()) }
+ data['def'] = jcast.getDefinition()
+ data['status'] = jcast.getStatus()
+ data['learn'] = 1;
+ logging.info("SCANNER------ LEARN MODE : New device : " + uuid + ' (' + data["friendly_name"] + ')')
+ globals.JEEDOM_COM.add_changes('devices::'+uuid,data)
+
+ for known in globals.KNOWN_DEVICES :
+ if known not in DISCOVER_DEVICES :
+ logging.info("SCANNER------No connection to device " + known)
+ if known in globals.GCAST_DEVICES :
+ globals.GCAST_DEVICES[known].disconnect()
+ del globals.GCAST_DEVICES[known]
+ globals.KNOWN_DEVICES[known]['lastScan'] = int(time.time())
+ #if ( (int(time.time())-globals.KNOWN_DEVICES[known]['lastOfflineSent']) > globals.LOSTDEVICE_RESENDNOTIFDELAY) :
+ if ( globals.KNOWN_DEVICES[known]['online']==True or (int(time.time())-globals.KNOWN_DEVICES[known]['lastOfflineSent'])>globals.LOSTDEVICE_RESENDNOTIFDELAY ) :
+ globals.KNOWN_DEVICES[known]['online'] = False
+ globals.KNOWN_DEVICES[known]['lastOfflineSent'] = int(time.time())
+ globals.KNOWN_DEVICES[known]['status'] = status = {
+ "uuid" : known,
+ "friendly_name" : "",
+ "is_stand_by" : False,
+ "app_id" : "",
+ "display_name" : "",
+ "status_text" : "",
+ "idle" : False,
+ }
+ #globals.JEEDOM_COM.add_changes('devices::'+known, globals.KNOWN_DEVICES[known])
+ #globals.JEEDOM_COM.send_change_immediate({'devices': [ globals.KNOWN_DEVICES[known] ] });
+ globals.JEEDOM_COM.send_change_immediate_device(known, globals.KNOWN_DEVICES[known]);
+ if known in globals.NOWPLAYING_DEVICES:
+ data = {
+ "uuid" : known,
+ "online" : False, "friendly_name" : "",
+ "is_active_input" : False, "is_stand_by" : False,
+ "app_id" : "", "display_name" : "", "status_text" : "",
+ "idle" : False, "title" : "",
+ "album_artist" : "","metadata_type" : "",
+ "album_name" : "", "current_time" : 0,
+ "artist" : "", "image" : None,
+ 'series_title': "", 'season': "", 'episode': "",
+ "stream_type" : "", "track" : "",
+ "player_state" : "","supported_media_commands" : 0,
+ "supports_pause" : "", 'duration': 0,
+ 'content_type': "", 'idle_reason': ""
+ }
+ globals.JEEDOM_COM.send_change_immediate({'uuid' : known, 'nowplaying':data});
+
+ else :
+ globals.KNOWN_DEVICES[known]["lastScan"] = int(time.time())
+
+ except Exception as e:
+ logging.error("SCANNER------Exception on scanner : %s" % str(e))
+ logging.debug(traceback.format_exc())
+
+ globals.SCAN_LAST = int(time.time())
+ globals.SCAN_PENDING = False
+
+
+def heartbeat_handler(delay):
+ while True:
+ if globals.LEARN_MODE and (globals.LEARN_BEGIN + globals.LEARN_TIMEOUT/2) < int(time.time()):
+ globals.LEARN_MODE = False
+ logging.debug('HEARTBEAT------Quitting learn mode (60s elapsed)')
+ globals.JEEDOM_COM.send_change_immediate({'learn_mode' : 0,'source' : globals.daemonname});
+
+ if (globals.LAST_BEAT + globals.HEARTBEAT_FREQUENCY/2 -5) < int(time.time()):
+ globals.JEEDOM_COM.send_change_immediate({'heartbeat' : 1,'source' : globals.daemonname});
+ globals.LAST_BEAT = int(time.time())
+
+ time.sleep(10) # every 10 secondes
+
+
+def handler(signum=None, frame=None):
+ logging.debug("GLOBAL------Signal %i caught, exiting..." % int(signum))
+ shutdown()
+
+def shutdown():
+ logging.debug("GLOBAL------Shutdown")
+ logging.debug("GLOBAL------Removing PID file " + str(globals.pidfile))
+ try:
+ os.remove(globals.pidfile)
+ except:
+ pass
+ try:
+ for uuid in globals.GCAST_DEVICES :
+ globals.GCAST_DEVICES[uuid].disconnect()
+ time.sleep(1)
+ jeedom_socket.close()
+ except:
+ pass
+ logging.debug("Exit 0")
+ sys.stdout.flush()
+ os._exit(0)
+
+# -------------------------------------------
+# ------ PROGRAM STARTS HERE ----------------
+# -------------------------------------------
+parser = argparse.ArgumentParser(description='Blead Daemon for Jeedom plugin')
+parser.add_argument("--device", help="Device", type=str)
+parser.add_argument("--loglevel", help="Log Level for the daemon", type=str)
+parser.add_argument("--pidfile", help="Value to write", type=str)
+parser.add_argument("--callback", help="Value to write", type=str)
+parser.add_argument("--apikey", help="Value to write", type=str)
+parser.add_argument("--socketport", help="Socket Port", type=str)
+parser.add_argument("--sockethost", help="Socket Host", type=str)
+parser.add_argument("--daemonname", help="Daemon Name", type=str)
+parser.add_argument("--cycle", help="Cycle to send event", type=str)
+args = parser.parse_args()
+
+if args.device:
+ globals.device = args.device
+if args.loglevel:
+ globals.log_level = args.loglevel
+if args.pidfile:
+ globals.pidfile = args.pidfile
+if args.callback:
+ globals.callback = args.callback
+if args.apikey:
+ globals.apikey = args.apikey
+if args.cycle:
+ globals.cycle = float(args.cycle)
+if args.socketport:
+ globals.socketport = args.socketport
+if args.sockethost:
+ globals.sockethost = args.sockethost
+if args.daemonname:
+ globals.daemonname = args.daemonname
+
+globals.socketport = int(globals.socketport)
+globals.cycle = float(globals.cycle)
+
+jeedom_utils.set_log_level(globals.log_level)
+logging.info('GLOBAL------Start googlecast')
+logging.info('GLOBAL------Log level : '+str(globals.log_level))
+logging.info('GLOBAL------Socket port : '+str(globals.socketport))
+logging.info('GLOBAL------Socket host : '+str(globals.sockethost))
+logging.info('GLOBAL------PID file : '+str(globals.pidfile))
+logging.info('GLOBAL------Apikey : '+str(globals.apikey))
+logging.info('GLOBAL------Callback : '+str(globals.callback))
+logging.info('GLOBAL------Cycle : '+str(globals.cycle))
+
+signal.signal(signal.SIGINT, handler)
+signal.signal(signal.SIGTERM, handler)
+#globals.IFACE_DEVICE = int(globals.device[-1:])
+try:
+ jeedom_utils.write_pid(str(globals.pidfile))
+ globals.JEEDOM_COM = jeedom_com(apikey = globals.apikey,url = globals.callback,cycle=globals.cycle)
+ if not globals.JEEDOM_COM.test():
+ logging.error('GLOBAL------Network communication issues. Please fix your Jeedom network configuration.')
+ shutdown()
+ else :
+ logging.info('GLOBAL------Network communication to jeedom OK.')
+ jeedom_socket = jeedom_socket(port=globals.socketport,address=globals.sockethost)
+ listen()
+except Exception as e:
+ logging.error('GLOBAL------Fatal error : '+str(e))
+ logging.debug(traceback.format_exc())
+ shutdown()
diff --git a/resources/install.sh b/resources/install.sh
new file mode 100644
index 0000000..dab9bf5
--- /dev/null
+++ b/resources/install.sh
@@ -0,0 +1,14 @@
+touch /tmp/dependancy_googlecast_in_progress
+echo 0 > /tmp/dependancy_googlecast_in_progress
+echo "Launch install of googlecast dependancy"
+sudo apt-get update
+echo 20 > /tmp/dependancy_googlecast_in_progress
+sudo apt-get install -y python3 python-dev build-essential
+echo 50 > /tmp/dependancy_googlecast_in_progress
+sudo apt-get install -y python-requests python3-pip
+echo 75 > /tmp/dependancy_googlecast_in_progress
+cd pychromecast
+sudo pip3 install -r requirements.txt
+echo 100 > /tmp/dependancy_googlecast_in_progress
+echo "Everything is successfully installed!"
+rm /tmp/dependancy_googlecast_in_progress
diff --git a/resources/jeedom/__init__.py b/resources/jeedom/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resources/jeedom/jeedom.py b/resources/jeedom/jeedom.py
new file mode 100644
index 0000000..90f84d1
--- /dev/null
+++ b/resources/jeedom/jeedom.py
@@ -0,0 +1,224 @@
+# This file is part of Jeedom.
+#
+# Jeedom is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Jeedom is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Jeedom. If not, see .
+#
+
+import time
+import logging
+import threading
+import _thread as thread
+import requests
+import datetime
+import collections
+import os
+from os.path import join
+import socket
+from multiprocessing import Queue
+import socketserver as SocketServer
+from socketserver import (TCPServer, StreamRequestHandler)
+import signal
+import unicodedata
+
+
+# ------------------------------------------------------------------------------
+
+class jeedom_com():
+ def __init__(self,apikey = '',url = '',cycle = 0.5,retry = 3):
+ self.apikey = apikey
+ self.url = url
+ self.cycle = cycle
+ self.retry = retry
+ self.changes = {}
+ if cycle > 0 :
+ self.send_changes_async()
+ logging.debug('Init request module v%s' % (str(requests.__version__),))
+
+ def send_changes_async(self):
+ try:
+ if len(self.changes) == 0:
+ resend_changes = threading.Timer(self.cycle, self.send_changes_async)
+ resend_changes.start()
+ return
+ start_time = datetime.datetime.now()
+ changes = self.changes
+ self.changes = {}
+ logging.debug('SENDER------Send to jeedom : '+str(changes))
+ i=0
+ while i < self.retry:
+ try:
+ r = requests.post(self.url + '?apikey=' + self.apikey, json=changes, timeout=(0.5, 120), verify=False)
+ if r.status_code == requests.codes.ok:
+ break
+ except Exception as error:
+ logging.error('SENDER------Error on send request to jeedom ' + str(error)+' retry : '+str(i)+'/'+str(self.retry))
+ i = i + 1
+ if r.status_code != requests.codes.ok:
+ logging.error('SENDER------Error on send request to jeedom, return code %s' % (str(r.status_code),))
+ dt = datetime.datetime.now() - start_time
+ ms = (dt.days * 24 * 60 * 60 + dt.seconds) * 1000 + dt.microseconds / 1000.0
+ timer_duration = self.cycle - ms
+ if timer_duration < 0.1:
+ timer_duration = 0.1
+ resend_changes = threading.Timer(timer_duration, self.send_changes_async)
+ resend_changes.start()
+ except Exception as error:
+ logging.error('SENDER------Critical error on send_changes_async %s' % (str(error),))
+ resend_changes = threading.Timer(self.cycle, self.send_changes_async)
+ resend_changes.start()
+
+ def add_changes(self,key,value):
+ if key.find('::') != -1:
+ tmp_changes = {}
+ changes = value
+ for k in reversed(key.split('::')):
+ if k not in tmp_changes:
+ tmp_changes[k] = {}
+ tmp_changes[k] = changes
+ changes = tmp_changes
+ tmp_changes = {}
+ if self.cycle <= 0:
+ self.send_change_immediate(changes)
+ else:
+ self.merge_dict(self.changes,changes)
+ else:
+ if self.cycle <= 0:
+ self.send_change_immediate({key:value})
+ else:
+ self.changes[key] = value
+
+ def send_change_immediate(self,change):
+ thread.start_new_thread( self.thread_change, (change,))
+
+ def send_change_immediate_device(self,uuid, change):
+ thread.start_new_thread( self.thread_change, ({'devices': {uuid: change}},))
+
+ def thread_change(self,change):
+ logging.debug('SENDER------Send to jeedom : %s' % (str(change),))
+ i=0
+ while i < self.retry:
+ try:
+ r = requests.post(self.url + '?apikey=' + self.apikey, json=change, timeout=(0.5, 120), verify=False)
+ if r.status_code == requests.codes.ok:
+ break
+ except Exception as error:
+ logging.error('SENDER------Error on send request to jeedom ' + str(error)+' retry : '+str(i)+'/'+str(self.retry))
+ i = i + 1
+
+ def set_change(self,changes):
+ self.changes = changes
+
+ def get_change(self):
+ return self.changes
+
+ def merge_dict(self,d1, d2):
+ for k,v2 in d2.items():
+ v1 = d1.get(k) # returns None if v1 has no value for this key
+ if ( isinstance(v1, collections.Mapping) and
+ isinstance(v2, collections.Mapping) ):
+ self.merge_dict(v1, v2)
+ else:
+ d1[k] = v2
+
+ def test(self):
+ try:
+ response = requests.get(self.url + '?apikey=' + self.apikey, verify=False)
+ if response.status_code != requests.codes.ok:
+ logging.error('SENDER------Callback error: %s %s. Please check your network configuration page'% (response.status.code, response.status.message,))
+ return False
+ except Exception as e:
+ logging.error('SENDER------Callback result as a unknown error: %s. Please check your network configuration page'% (e.message,))
+ return False
+ return True
+
+# ------------------------------------------------------------------------------
+
+class jeedom_utils():
+
+ @staticmethod
+ def convert_log_level(level = 'error'):
+ LEVELS = {'debug': logging.DEBUG,
+ 'info': logging.INFO,
+ 'notice': logging.WARNING,
+ 'warning': logging.WARNING,
+ 'error': logging.ERROR,
+ 'critical': logging.CRITICAL,
+ 'none': logging.NOTSET}
+ return LEVELS.get(level, logging.NOTSET)
+
+ @staticmethod
+ def set_log_level(level = 'error'):
+ FORMAT = '[%(asctime)-15s][%(levelname)s] : %(message)s'
+
+ if level=='debug' :
+ logging.getLogger("pychromecast").setLevel(logging.ERROR)
+ logging.getLogger("urllib3").setLevel(logging.ERROR)
+ else :
+ logging.getLogger("pychromecast").setLevel(logging.CRITICAL)
+ logging.getLogger("urllib3").setLevel(logging.CRITICAL)
+
+ logging.basicConfig(level=jeedom_utils.convert_log_level(level),format=FORMAT, datefmt="%Y-%m-%d %H:%M:%S")
+
+ @staticmethod
+ def write_pid(path):
+ pid = str(os.getpid())
+ logging.debug("Writing PID " + pid + " to " + str(path))
+ open(path, 'w').write("%s\n" % pid)
+
+# ------------------------------------------------------------------------------
+
+
+# ------------------------------------------------------------------------------
+
+JEEDOM_SOCKET_MESSAGE = Queue()
+
+class jeedom_socket_handler(StreamRequestHandler):
+ def handle(self):
+ global JEEDOM_SOCKET_MESSAGE
+ logging.debug("SOCKETHANDLER------Client connected to [%s:%d]" % self.client_address)
+ lg = self.rfile.readline().strip().decode("utf-8")
+ JEEDOM_SOCKET_MESSAGE.put(lg)
+ logging.debug("SOCKETHANDLER------Message read from socket: " + lg)
+ self.netAdapterClientConnected = False
+ logging.debug("SOCKETHANDLER------Client disconnected from [%s:%d]" % self.client_address)
+
+class jeedom_socket():
+
+ def __init__(self,address='localhost', port=55000):
+ self.address = address
+ self.port = port
+ SocketServer.TCPServer.allow_reuse_address = True
+
+ def open(self):
+ self.netAdapter = TCPServer((self.address, self.port), jeedom_socket_handler)
+ if self.netAdapter:
+ logging.debug("SOCKETHANDLER------Socket interface started")
+ threading.Thread(target=self.loopNetServer, args=()).start()
+ else:
+ logging.debug("SOCKETHANDLER------Cannot start socket interface")
+
+ def loopNetServer(self):
+ logging.debug("SOCKETHANDLER------LoopNetServer Thread started")
+ logging.debug("SOCKETHANDLER------Listening on: [%s:%d]" % (self.address, self.port))
+ self.netAdapter.serve_forever()
+ logging.debug("SOCKETHANDLER------LoopNetServer Thread stopped")
+
+ def close(self):
+ self.netAdapter.shutdown()
+
+ def getMessage(self):
+ return self.message
+
+# ------------------------------------------------------------------------------
+# END
+# ------------------------------------------------------------------------------
diff --git a/resources/pychromecast/.gitignore b/resources/pychromecast/.gitignore
new file mode 100644
index 0000000..a49dcb8
--- /dev/null
+++ b/resources/pychromecast/.gitignore
@@ -0,0 +1,53 @@
+# Hide sublime text stuff
+*.sublime-project
+*.sublime-workspace
+
+# Hide some OS X stuff
+.DS_Store
+.AppleDouble
+.LSOverride
+Icon
+
+# Thumbnails
+._*
+
+
+# GITHUB Proposed Python stuff:
+*.py[cod]
+
+# C extensions
+*.so
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+lib
+lib64
+
+# Build files
+README.rst
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+nosetests.xml
+
+# Translations
+*.mo
+
+# Mr Developer
+.mr.developer.cfg
+.project
+.pydevproject
\ No newline at end of file
diff --git a/resources/pychromecast/.travis.yml b/resources/pychromecast/.travis.yml
new file mode 100644
index 0000000..730ec45
--- /dev/null
+++ b/resources/pychromecast/.travis.yml
@@ -0,0 +1,12 @@
+sudo: false
+language: python
+python:
+ - "3.4"
+ - "3.5"
+ - "3.6"
+install:
+ - pip install -r requirements.txt
+ - pip install flake8==3.3.0 pylint==1.8.1
+script:
+ - flake8 --exclude cast_channel_pb2.py,authority_keys_pb2.py,logging_pb2.py pychromecast
+ - pylint pychromecast
diff --git a/resources/pychromecast/LICENSE b/resources/pychromecast/LICENSE
new file mode 100644
index 0000000..b3c5e1d
--- /dev/null
+++ b/resources/pychromecast/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 Paulus Schoutsen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/resources/pychromecast/MANIFEST.in b/resources/pychromecast/MANIFEST.in
new file mode 100644
index 0000000..a49e586
--- /dev/null
+++ b/resources/pychromecast/MANIFEST.in
@@ -0,0 +1,5 @@
+include README.rst
+include LICENSE
+include requirements.txt
+graft pychromecast
+recursive-exclude * *.py[co]
diff --git a/resources/pychromecast/__init__.py b/resources/pychromecast/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resources/pychromecast/chromecast_protobuf/README.md b/resources/pychromecast/chromecast_protobuf/README.md
new file mode 100644
index 0000000..a345567
--- /dev/null
+++ b/resources/pychromecast/chromecast_protobuf/README.md
@@ -0,0 +1 @@
+These files were imported from https://chromium.googlesource.com/chromium/src.git/+/master/extensions/common/api/cast_channel to generate the \_pb2.py-files.
\ No newline at end of file
diff --git a/resources/pychromecast/chromecast_protobuf/authority_keys.proto b/resources/pychromecast/chromecast_protobuf/authority_keys.proto
new file mode 100644
index 0000000..791e831
--- /dev/null
+++ b/resources/pychromecast/chromecast_protobuf/authority_keys.proto
@@ -0,0 +1,17 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package extensions.api.cast_channel.proto;
+
+message AuthorityKeys {
+ message Key {
+ required bytes fingerprint = 1;
+ required bytes public_key = 2;
+ }
+ repeated Key keys = 1;
+}
diff --git a/resources/pychromecast/chromecast_protobuf/cast_channel.proto b/resources/pychromecast/chromecast_protobuf/cast_channel.proto
new file mode 100644
index 0000000..098ecb7
--- /dev/null
+++ b/resources/pychromecast/chromecast_protobuf/cast_channel.proto
@@ -0,0 +1,101 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package extensions.api.cast_channel;
+
+message CastMessage {
+ // Always pass a version of the protocol for future compatibility
+ // requirements.
+ enum ProtocolVersion {
+ CASTV2_1_0 = 0;
+ }
+ required ProtocolVersion protocol_version = 1;
+
+ // source and destination ids identify the origin and destination of the
+ // message. They are used to route messages between endpoints that share a
+ // device-to-device channel.
+ //
+ // For messages between applications:
+ // - The sender application id is a unique identifier generated on behalf of
+ // the sender application.
+ // - The receiver id is always the the session id for the application.
+ //
+ // For messages to or from the sender or receiver platform, the special ids
+ // 'sender-0' and 'receiver-0' can be used.
+ //
+ // For messages intended for all endpoints using a given channel, the
+ // wildcard destination_id '*' can be used.
+ required string source_id = 2;
+ required string destination_id = 3;
+
+ // This is the core multiplexing key. All messages are sent on a namespace
+ // and endpoints sharing a channel listen on one or more namespaces. The
+ // namespace defines the protocol and semantics of the message.
+ required string namespace = 4;
+
+ // Encoding and payload info follows.
+
+ // What type of data do we have in this message.
+ enum PayloadType {
+ STRING = 0;
+ BINARY = 1;
+ }
+ required PayloadType payload_type = 5;
+
+ // Depending on payload_type, exactly one of the following optional fields
+ // will always be set.
+ optional string payload_utf8 = 6;
+ optional bytes payload_binary = 7;
+}
+
+enum SignatureAlgorithm {
+ UNSPECIFIED = 0;
+ RSASSA_PKCS1v15 = 1;
+ RSASSA_PSS = 2;
+}
+
+enum HashAlgorithm {
+ SHA1 = 0;
+ SHA256 = 1;
+}
+
+// Messages for authentication protocol between a sender and a receiver.
+message AuthChallenge {
+ optional SignatureAlgorithm signature_algorithm = 1
+ [default = RSASSA_PKCS1v15];
+ optional bytes sender_nonce = 2;
+ optional HashAlgorithm hash_algorithm = 3 [default = SHA1];
+}
+
+message AuthResponse {
+ required bytes signature = 1;
+ required bytes client_auth_certificate = 2;
+ repeated bytes intermediate_certificate = 3;
+ optional SignatureAlgorithm signature_algorithm = 4
+ [default = RSASSA_PKCS1v15];
+ optional bytes sender_nonce = 5;
+ optional HashAlgorithm hash_algorithm = 6 [default = SHA1];
+ optional bytes crl = 7;
+}
+
+message AuthError {
+ enum ErrorType {
+ INTERNAL_ERROR = 0;
+ NO_TLS = 1; // The underlying connection is not TLS
+ SIGNATURE_ALGORITHM_UNAVAILABLE = 2;
+ }
+ required ErrorType error_type = 1;
+}
+
+message DeviceAuthMessage {
+ // Request fields
+ optional AuthChallenge challenge = 1;
+ // Response fields
+ optional AuthResponse response = 2;
+ optional AuthError error = 3;
+}
diff --git a/resources/pychromecast/chromecast_protobuf/logging.proto b/resources/pychromecast/chromecast_protobuf/logging.proto
new file mode 100644
index 0000000..a199c01
--- /dev/null
+++ b/resources/pychromecast/chromecast_protobuf/logging.proto
@@ -0,0 +1,172 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package extensions.api.cast_channel.proto;
+
+enum EventType {
+ EVENT_TYPE_UNKNOWN = 0;
+ CAST_SOCKET_CREATED = 1;
+ READY_STATE_CHANGED = 2;
+ CONNECTION_STATE_CHANGED = 3;
+ READ_STATE_CHANGED = 4;
+ WRITE_STATE_CHANGED = 5;
+ ERROR_STATE_CHANGED = 6;
+ CONNECT_FAILED = 7;
+ TCP_SOCKET_CONNECT = 8; // Logged with RV.
+ TCP_SOCKET_SET_KEEP_ALIVE = 9;
+ SSL_CERT_WHITELISTED = 10;
+ SSL_SOCKET_CONNECT = 11; // Logged with RV.
+ SSL_INFO_OBTAINED = 12;
+ DER_ENCODED_CERT_OBTAIN = 13; // Logged with RV.
+ RECEIVED_CHALLENGE_REPLY = 14;
+ AUTH_CHALLENGE_REPLY = 15;
+ CONNECT_TIMED_OUT = 16;
+ SEND_MESSAGE_FAILED = 17;
+ MESSAGE_ENQUEUED = 18; // Message
+ SOCKET_WRITE = 19; // Logged with RV.
+ MESSAGE_WRITTEN = 20; // Message
+ SOCKET_READ = 21; // Logged with RV.
+ MESSAGE_READ = 22; // Message
+ SOCKET_CLOSED = 25;
+ SSL_CERT_EXCESSIVE_LIFETIME = 26;
+ CHANNEL_POLICY_ENFORCED = 27;
+ TCP_SOCKET_CONNECT_COMPLETE = 28; // Logged with RV.
+ SSL_SOCKET_CONNECT_COMPLETE = 29; // Logged with RV.
+ SSL_SOCKET_CONNECT_FAILED = 30; // Logged with RV.
+ SEND_AUTH_CHALLENGE_FAILED = 31; // Logged with RV.
+ AUTH_CHALLENGE_REPLY_INVALID = 32;
+ PING_WRITE_ERROR = 33; // Logged with RV.
+}
+
+enum ChannelAuth {
+ // SSL over TCP.
+ SSL = 1;
+ // SSL over TCP with challenge and receiver signature verification.
+ SSL_VERIFIED = 2;
+}
+
+enum ReadyState {
+ READY_STATE_NONE = 1;
+ READY_STATE_CONNECTING = 2;
+ READY_STATE_OPEN = 3;
+ READY_STATE_CLOSING = 4;
+ READY_STATE_CLOSED = 5;
+}
+
+enum ConnectionState {
+ CONN_STATE_UNKNOWN = 1;
+ CONN_STATE_TCP_CONNECT = 2;
+ CONN_STATE_TCP_CONNECT_COMPLETE = 3;
+ CONN_STATE_SSL_CONNECT = 4;
+ CONN_STATE_SSL_CONNECT_COMPLETE = 5;
+ CONN_STATE_AUTH_CHALLENGE_SEND = 6;
+ CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE = 7;
+ CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE = 8;
+ CONN_STATE_START_CONNECT = 9;
+
+ // Terminal states follow.
+ CONN_STATE_FINISHED = 100;
+ CONN_STATE_ERROR = 101;
+ CONN_STATE_TIMEOUT = 102;
+}
+
+enum ReadState {
+ READ_STATE_UNKNOWN = 1;
+ READ_STATE_READ = 2;
+ READ_STATE_READ_COMPLETE = 3;
+ READ_STATE_DO_CALLBACK = 4;
+ READ_STATE_HANDLE_ERROR = 5;
+ READ_STATE_ERROR = 100; // Terminal state.
+}
+
+enum WriteState {
+ WRITE_STATE_UNKNOWN = 1;
+ WRITE_STATE_WRITE = 2;
+ WRITE_STATE_WRITE_COMPLETE = 3;
+ WRITE_STATE_DO_CALLBACK = 4;
+ WRITE_STATE_HANDLE_ERROR = 5;
+
+ // Terminal states follow.
+ WRITE_STATE_ERROR = 100;
+ WRITE_STATE_IDLE = 101;
+}
+
+enum ErrorState {
+ CHANNEL_ERROR_NONE = 1;
+ CHANNEL_ERROR_CHANNEL_NOT_OPEN = 2;
+ CHANNEL_ERROR_AUTHENTICATION_ERROR = 3;
+ CHANNEL_ERROR_CONNECT_ERROR = 4;
+ CHANNEL_ERROR_SOCKET_ERROR = 5;
+ CHANNEL_ERROR_TRANSPORT_ERROR = 6;
+ CHANNEL_ERROR_INVALID_MESSAGE = 7;
+ CHANNEL_ERROR_INVALID_CHANNEL_ID = 8;
+ CHANNEL_ERROR_CONNECT_TIMEOUT = 9;
+ CHANNEL_ERROR_UNKNOWN = 10;
+}
+
+enum ChallengeReplyErrorType {
+ CHALLENGE_REPLY_ERROR_NONE = 1;
+ CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY = 2;
+ CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE = 3;
+ CHALLENGE_REPLY_ERROR_NO_PAYLOAD = 4;
+ CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED = 5;
+ CHALLENGE_REPLY_ERROR_MESSAGE_ERROR = 6;
+ CHALLENGE_REPLY_ERROR_NO_RESPONSE = 7;
+ CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND = 8;
+ CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED = 9;
+ CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA = 10;
+ CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY = 11;
+ CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH = 12;
+ CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG = 13;
+ CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE = 14;
+ CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED = 15;
+ CHALLENGE_REPLY_ERROR_CRL_INVALID = 16;
+ CHALLENGE_REPLY_ERROR_CERT_REVOKED = 17;
+}
+
+message SocketEvent {
+ // Required
+ optional EventType type = 1;
+ optional int64 timestamp_micros = 2;
+
+ optional string details = 3;
+
+ optional int32 net_return_value = 4;
+
+ optional string message_namespace = 5;
+
+ optional ReadyState ready_state = 6;
+ optional ConnectionState connection_state = 7;
+ optional ReadState read_state = 8;
+ optional WriteState write_state = 9;
+ optional ErrorState error_state = 10;
+
+ optional ChallengeReplyErrorType challenge_reply_error_type = 11;
+ // No longer used.
+ optional int32 nss_error_code = 12;
+}
+
+message AggregatedSocketEvent {
+ optional int32 id = 1;
+ optional int32 endpoint_id = 2;
+ optional ChannelAuth channel_auth_type = 3;
+ repeated SocketEvent socket_event = 4;
+ optional int64 bytes_read = 5;
+ optional int64 bytes_written = 6;
+}
+
+message Log {
+ // Each AggregatedSocketEvent represents events recorded for a socket.
+ repeated AggregatedSocketEvent aggregated_socket_event = 1;
+
+ // Number of socket log entries evicted by the logger due to size constraints.
+ optional int32 num_evicted_aggregated_socket_events = 2;
+
+ // Number of event log entries evicted by the logger due to size constraints.
+ optional int32 num_evicted_socket_events = 3;
+}
diff --git a/resources/pychromecast/examples/blocking.py b/resources/pychromecast/examples/blocking.py
new file mode 100644
index 0000000..15a353a
--- /dev/null
+++ b/resources/pychromecast/examples/blocking.py
@@ -0,0 +1,69 @@
+"""
+Example that shows how the socket client can be used.
+
+Functions called in this example are blocking which means that
+the function doesn't return as long as no result was received.
+"""
+import time
+import sys
+import logging
+
+import pychromecast
+import pychromecast.controllers.youtube as youtube
+
+if '--show-debug' in sys.argv:
+ logging.basicConfig(level=logging.DEBUG)
+
+casts = pychromecast.get_chromecasts()
+if len(casts) == 0:
+ print("No Devices Found")
+ exit()
+cast = casts[0]
+
+print()
+print(cast.device)
+time.sleep(1)
+print()
+print(cast.status)
+print()
+print(cast.media_controller.status)
+print()
+
+if '--show-status-only' in sys.argv:
+ sys.exit()
+
+if not cast.is_idle:
+ print("Killing current running app")
+ cast.quit_app()
+ time.sleep(5)
+
+print("Playing media")
+cast.play_media(
+ ("http://commondatastorage.googleapis.com/gtv-videos-bucket/"
+ "sample/BigBuckBunny.mp4"), "video/mp4")
+
+t = 0
+
+while True:
+ try:
+ t += 1
+
+ if t > 10 and t % 3 == 0:
+ print("Media status", cast.media_controller.status)
+
+ if t == 15:
+ print("Sending pause command")
+ cast.media_controller.pause()
+ elif t == 20:
+ print("Sending play command")
+ cast.media_controller.play()
+ elif t == 25:
+ print("Sending stop command")
+ cast.media_controller.stop()
+ elif t == 32:
+ cast.quit_app()
+ break
+
+ time.sleep(1)
+ except KeyboardInterrupt:
+ break
diff --git a/resources/pychromecast/examples/dashcast_blocking.py b/resources/pychromecast/examples/dashcast_blocking.py
new file mode 100644
index 0000000..e2e7643
--- /dev/null
+++ b/resources/pychromecast/examples/dashcast_blocking.py
@@ -0,0 +1,56 @@
+"""
+Example that shows how the DashCast controller can be used.
+
+Functions called in this example are blocking which means that
+the function doesn't return as long as no result was received.
+"""
+import time
+import sys
+import logging
+
+import pychromecast
+import pychromecast.controllers.dashcast as dashcast
+
+debug = '--show-debug' in sys.argv
+if debug:
+ logging.basicConfig(level=logging.DEBUG)
+
+casts = pychromecast.get_chromecasts()
+if len(casts) == 0:
+ print("No Devices Found")
+ exit()
+
+cast = casts[0]
+
+d = dashcast.DashCastController()
+cast.register_handler(d)
+
+print()
+print(cast.device)
+time.sleep(1)
+print()
+print(cast.status)
+print()
+print(cast.media_controller.status)
+print()
+
+if not cast.is_idle:
+ print("Killing current running app")
+ cast.quit_app()
+ time.sleep(5)
+
+time.sleep(1)
+
+# Test that the callback chain works. This should send a message to
+# load the first url, but immediately after send a message load the
+# second url.
+warning_message = 'If you see this on your TV then something is broken'
+d.load_url('https://home-assistant.io/? ' + warning_message,
+ callback_function=lambda result:
+ d.load_url('https://home-assistant.io/'))
+
+
+# If debugging sleep after running so we can see any error messages.
+if debug:
+ time.sleep(10)
+
diff --git a/resources/pychromecast/examples/non_blocking.py b/resources/pychromecast/examples/non_blocking.py
new file mode 100644
index 0000000..863108c
--- /dev/null
+++ b/resources/pychromecast/examples/non_blocking.py
@@ -0,0 +1,80 @@
+"""
+Example that shows how the socket client can be used.
+
+All functions (except get_chromecast()) are non-blocking and
+return immediately without waiting for the result. You can use
+that functionality to include pychromecast into your main loop.
+"""
+import time
+import select
+import sys
+import logging
+
+import pychromecast
+
+"""
+Check for cast.socket_client.get_socket() and
+handle it with cast.socket_client.run_once()
+"""
+def your_main_loop():
+ t = 1
+ cast = None
+ def callback(chromecast):
+ nonlocal cast
+ cast = chromecast
+ stop_discovery()
+
+ stop_discovery = pychromecast.get_chromecasts(blocking=False, callback=callback)
+
+ while True:
+ if cast:
+ polltime = 0.1
+ can_read, _, _ = select.select([cast.socket_client.get_socket()], [], [], polltime)
+ if can_read:
+ #received something on the socket, handle it with run_once()
+ cast.socket_client.run_once()
+ do_actions(cast, t)
+ t += 1
+ if(t > 50):
+ break
+ else:
+ print("=> Waiting for cast discovery...")
+ time.sleep(1)
+
+"""
+Your code which is called by main loop
+"""
+def do_actions(cast, t):
+ if t == 5:
+ print()
+ print("=> Sending non-blocking play_media command")
+ cast.play_media(
+ ("http://commondatastorage.googleapis.com/gtv-videos-bucket/"
+ "sample/BigBuckBunny.mp4"), "video/mp4")
+ elif t == 30:
+ print()
+ print("=> Sending non-blocking pause command")
+ cast.media_controller.pause()
+ elif t == 35:
+ print()
+ print("=> Sending non-blocking play command")
+ cast.media_controller.play()
+ elif t == 40:
+ print()
+ print("=> Sending non-blocking stop command")
+ cast.media_controller.stop()
+ elif t == 45:
+ print()
+ print("=> Sending non-blocking quit_app command")
+ cast.quit_app()
+ elif t % 4 == 0:
+ print()
+ print("Media status", cast.media_controller.status)
+
+if '--show-debug' in sys.argv:
+ logging.basicConfig(level=logging.DEBUG)
+else:
+ logging.basicConfig(level=logging.INFO)
+
+your_main_loop()
+
diff --git a/resources/pychromecast/examples/spotify_example.py b/resources/pychromecast/examples/spotify_example.py
new file mode 100644
index 0000000..c0accf1
--- /dev/null
+++ b/resources/pychromecast/examples/spotify_example.py
@@ -0,0 +1,38 @@
+"""
+Example on how to use the Spotify Controller.
+NOTE: You need to install the spotipy and spotify-token dependencies.
+
+This can be done by running the following:
+pip install spotify-token
+pip install git+https://github.com/plamere/spotipy.git
+"""
+import pychromecast
+from pychromecast.controllers.spotify import SpotifyController
+import spotify_token as st
+import spotipy
+
+chromecasts = pychromecast.get_chromecasts()
+cast = chromecasts[0]
+
+CAST_NAME = "My Chromecast"
+device_id = None
+
+if cast.name == CAST_NAME:
+
+ data = st.start_session("SPOTIFY_USERNAME", "SPOTIFY_PASSWORD")
+ access_token = data[0]
+
+ client = spotipy.Spotify(auth=access_token)
+
+ sp = SpotifyController(access_token)
+ cast.register_handler(sp)
+ sp.launch_app()
+
+ devices_available = client.devices()
+
+ for device in devices_available['devices']:
+ if device['name'] == CAST_NAME and device['type'] == 'CastVideo':
+ device_id = device['id']
+ break
+
+ client.start_playback(device_id=device_id, uris=["spotify:track:3Zwu2K0Qa5sT6teCCHPShP"])
\ No newline at end of file
diff --git a/resources/pychromecast/fabfile.py b/resources/pychromecast/fabfile.py
new file mode 100644
index 0000000..8525cb6
--- /dev/null
+++ b/resources/pychromecast/fabfile.py
@@ -0,0 +1,25 @@
+import os
+from fabric.decorators import task
+from fabric.operations import local
+
+
+@task
+def build():
+ """
+ Builds the distribution files
+ """
+ if not os.path.exists("build"):
+ os.mkdir("build")
+ local("date >> build/log")
+ local("python setup.py sdist >> build/log")
+ local("python setup.py bdist_wheel >> build/log")
+
+
+@task
+def release():
+ """
+ Uploads files to PyPi to create a new release.
+
+ Note: Requires that files have been built first
+ """
+ local("twine upload dist/*")
diff --git a/resources/pychromecast/pychromecast/__init__.py b/resources/pychromecast/pychromecast/__init__.py
new file mode 100644
index 0000000..6df5f16
--- /dev/null
+++ b/resources/pychromecast/pychromecast/__init__.py
@@ -0,0 +1,348 @@
+"""
+PyChromecast: remote control your Chromecast
+"""
+import logging
+import fnmatch
+
+# pylint: disable=wildcard-import
+import threading
+from .config import * # noqa
+from .error import * # noqa
+from . import socket_client
+from .discovery import discover_chromecasts, start_discovery, stop_discovery
+from .dial import get_device_status, reboot, DeviceStatus, CAST_TYPES, \
+ CAST_TYPE_CHROMECAST
+from .controllers.media import STREAM_TYPE_BUFFERED # noqa
+
+__all__ = (
+ '__version__', '__version_info__', 'get_chromecasts', 'Chromecast',
+)
+__version_info__ = ('0', '7', '6')
+__version__ = '.'.join(__version_info__)
+
+IDLE_APP_ID = 'E8C28D3C'
+IGNORE_CEC = []
+
+
+def _get_chromecast_from_host(host, tries=None, retry_wait=None, timeout=None,
+ blocking=True):
+ """Creates a Chromecast object from a zeroconf host."""
+ # Build device status from the mDNS info, this information is
+ # the primary source and the remaining will be fetched
+ # later on.
+ ip_address, port, uuid, model_name, friendly_name = host
+ cast_type = CAST_TYPES.get(model_name.lower(),
+ CAST_TYPE_CHROMECAST)
+ device = DeviceStatus(
+ friendly_name=friendly_name, model_name=model_name,
+ manufacturer=None, uuid=uuid, cast_type=cast_type,
+ )
+ return Chromecast(host=ip_address, port=port, device=device, tries=tries,
+ timeout=timeout, retry_wait=retry_wait,
+ blocking=blocking)
+
+
+# pylint: disable=too-many-locals
+def get_chromecasts(tries=None, retry_wait=None, timeout=None,
+ blocking=True, callback=None):
+ """
+ Searches the network for chromecast devices.
+
+ If blocking = True, returns a list of discovered chromecast devices.
+ If blocking = False, triggers a callback for each discovered chromecast,
+ and returns a function which can be executed to stop
+ discovery.
+
+ ex: get_chromecasts(friendly_name="Living Room")
+
+ May return an empty list if no chromecasts were found.
+
+ Tries is specified if you want to limit the number of times the
+ underlying socket associated with your Chromecast objects will
+ retry connecting if connection is lost or it fails to connect
+ in the first place. The number of seconds spent between each retry
+ can be defined by passing the retry_wait parameter, the default is
+ to wait 5 seconds.
+ """
+ if blocking:
+ # Thread blocking chromecast discovery
+ hosts = discover_chromecasts()
+ cc_list = []
+ for host in hosts:
+ try:
+ cc_list.append(_get_chromecast_from_host(
+ host, tries=tries, retry_wait=retry_wait, timeout=timeout,
+ blocking=blocking))
+ except ChromecastConnectionError: # noqa
+ pass
+ return cc_list
+ else:
+ # Callback based chromecast discovery
+ if not callable(callback):
+ raise ValueError(
+ "Nonblocking discovery requires a callback function.")
+
+ def internal_callback(name):
+ """Called when zeroconf has discovered a new chromecast."""
+ try:
+ callback(_get_chromecast_from_host(
+ listener.services[name], tries=tries,
+ retry_wait=retry_wait, timeout=timeout, blocking=blocking))
+ except ChromecastConnectionError: # noqa
+ pass
+
+ def internal_stop():
+ """Stops discovery of new chromecasts."""
+ stop_discovery(browser)
+
+ listener, browser = start_discovery(internal_callback)
+ return internal_stop
+
+
+# pylint: disable=too-many-instance-attributes
+class Chromecast(object):
+ """
+ Class to interface with a ChromeCast.
+
+ :param port: The port to use when connecting to the device, set to None to
+ use the default of 8009. Special devices such as Cast Groups
+ may return a different port number so we need to use that.
+ :param device: DeviceStatus with initial information for the device.
+ :type device: pychromecast.dial.DeviceStatus
+ :param tries: Number of retries to perform if the connection fails.
+ None for inifinite retries.
+ :param timeout: A floating point number specifying the socket timeout in
+ seconds. None means to use the default which is 30 seconds.
+ :param retry_wait: A floating point number specifying how many seconds to
+ wait between each retry. None means to use the default
+ which is 5 seconds.
+ """
+
+ def __init__(self, host, port=None, device=None, **kwargs):
+ tries = kwargs.pop('tries', None)
+ timeout = kwargs.pop('timeout', None)
+ retry_wait = kwargs.pop('retry_wait', None)
+ blocking = kwargs.pop('blocking', True)
+
+ self.logger = logging.getLogger(__name__)
+
+ # Resolve host to IP address
+ self.host = host
+ self.port = port or 8009
+
+ self.logger.info("Querying device status")
+ self.device = device
+ if device:
+ dev_status = get_device_status(self.host)
+ if dev_status:
+ # Values from `device` have priority over `dev_status`
+ # as they come from the dial information.
+ # `dev_status` may add extra information such as `manufacturer`
+ # which dial does not supply
+ self.device = DeviceStatus(
+ friendly_name=(device.friendly_name or
+ dev_status.friendly_name),
+ model_name=(device.model_name or
+ dev_status.model_name),
+ manufacturer=(device.manufacturer or
+ dev_status.manufacturer),
+ uuid=(device.uuid or
+ dev_status.uuid),
+ cast_type=(device.cast_type or
+ dev_status.cast_type),
+ )
+ else:
+ self.device = device
+ else:
+ self.device = get_device_status(self.host)
+
+ if not self.device:
+ raise ChromecastConnectionError( # noqa
+ "Could not connect to {}:{}".format(self.host, self.port))
+
+ self.status = None
+ self.status_event = threading.Event()
+
+ self.socket_client = socket_client.SocketClient(
+ host, port=port, cast_type=self.device.cast_type,
+ tries=tries, timeout=timeout, retry_wait=retry_wait,
+ blocking=blocking)
+
+ receiver_controller = self.socket_client.receiver_controller
+ receiver_controller.register_status_listener(self)
+
+ # Forward these methods
+ self.set_volume = receiver_controller.set_volume
+ self.set_volume_muted = receiver_controller.set_volume_muted
+ self.play_media = self.socket_client.media_controller.play_media
+ self.register_handler = self.socket_client.register_handler
+ self.register_status_listener = \
+ receiver_controller.register_status_listener
+ self.register_launch_error_listener = \
+ receiver_controller.register_launch_error_listener
+ self.register_connection_listener = \
+ self.socket_client.register_connection_listener
+
+ if blocking:
+ self.socket_client.start()
+
+ @property
+ def ignore_cec(self):
+ """ Returns whether the CEC data should be ignored. """
+ return self.device is not None and \
+ any([fnmatch.fnmatchcase(self.device.friendly_name, pattern)
+ for pattern in IGNORE_CEC])
+
+ @property
+ def is_idle(self):
+ """ Returns if there is currently an app running. """
+ return (self.status is None or
+ self.app_id in (None, IDLE_APP_ID) or
+ (not self.status.is_active_input and not self.ignore_cec))
+
+ @property
+ def uuid(self):
+ """ Returns the unique UUID of the Chromecast device. """
+ return self.device.uuid
+
+ @property
+ def name(self):
+ """
+ Returns the friendly name set for the Chromecast device.
+ This is the name that the end-user chooses for the cast device.
+ """
+ return self.device.friendly_name
+
+ @property
+ def uri(self):
+ """ Returns the device URI (ip:port) """
+ return "{}:{}".format(self.host, self.port)
+
+ @property
+ def model_name(self):
+ """ Returns the model name of the Chromecast device. """
+ return self.device.model_name
+
+ @property
+ def cast_type(self):
+ """
+ Returns the type of the Chromecast device.
+ This is one of CAST_TYPE_CHROMECAST for regular Chromecast device,
+ CAST_TYPE_AUDIO for Chromecast devices that only support audio
+ and CAST_TYPE_GROUP for virtual a Chromecast device that groups
+ together two or more cast (Audio for now) devices.
+
+ :rtype: str
+ """
+ return self.device.cast_type
+
+ @property
+ def app_id(self):
+ """ Returns the current app_id. """
+ return self.status.app_id if self.status else None
+
+ @property
+ def app_display_name(self):
+ """ Returns the name of the current running app. """
+ return self.status.display_name if self.status else None
+
+ @property
+ def media_controller(self):
+ """ Returns the media controller. """
+ return self.socket_client.media_controller
+
+ def new_cast_status(self, status):
+ """ Called when a new status received from the Chromecast. """
+ self.status = status
+ if status:
+ self.status_event.set()
+
+ def start_app(self, app_id):
+ """ Start an app on the Chromecast. """
+ self.logger.info("Starting app %s", app_id)
+
+ self.socket_client.receiver_controller.launch_app(app_id)
+
+ def quit_app(self):
+ """ Tells the Chromecast to quit current app_id. """
+ self.logger.info("Quiting current app")
+
+ self.socket_client.receiver_controller.stop_app()
+
+ def reboot(self):
+ """ Reboots the Chromecast. """
+ reboot(self.host)
+
+ def volume_up(self, delta=0.1):
+ """ Increment volume by 0.1 (or delta) unless it is already maxed.
+ Returns the new volume.
+
+ """
+ if delta <= 0:
+ raise ValueError(
+ "volume delta must be greater than zero, not {}".format(delta))
+ return self.set_volume(self.status.volume_level + delta)
+
+ def volume_down(self, delta=0.1):
+ """ Decrement the volume by 0.1 (or delta) unless it is already 0.
+ Returns the new volume.
+ """
+ if delta <= 0:
+ raise ValueError(
+ "volume delta must be greater than zero, not {}".format(delta))
+ return self.set_volume(self.status.volume_level - delta)
+
+ def wait(self, timeout=None):
+ """
+ Waits until the cast device is ready for communication. The device
+ is ready as soon a status message has been received.
+
+ If the status has already been received then the method returns
+ immediately.
+
+ :param timeout: a floating point number specifying a timeout for the
+ operation in seconds (or fractions thereof). Or None
+ to block forever.
+ """
+ self.status_event.wait(timeout=timeout)
+
+ def disconnect(self, timeout=None, blocking=True):
+ """
+ Disconnects the chromecast and waits for it to terminate.
+
+ :param timeout: a floating point number specifying a timeout for the
+ operation in seconds (or fractions thereof). Or None
+ to block forever.
+ :param blocking: If True it will block until the disconnection is
+ complete, otherwise it will return immediately.
+ """
+ self.socket_client.disconnect()
+ if blocking:
+ self.join(timeout=timeout)
+
+ def join(self, timeout=None):
+ """
+ Blocks the thread of the caller until the chromecast connection is
+ stopped.
+
+ :param timeout: a floating point number specifying a timeout for the
+ operation in seconds (or fractions thereof). Or None
+ to block forever.
+ """
+ self.socket_client.join(timeout=timeout)
+
+ def __del__(self):
+ try:
+ self.socket_client.stop.set()
+ except AttributeError:
+ pass
+
+ def __repr__(self):
+ txt = "Chromecast({!r}, port={!r}, device={!r})".format(
+ self.host, self.port, self.device)
+ return txt
+
+ def __unicode__(self):
+ return "Chromecast({}, {}, {}, {}, {})".format(
+ self.host, self.port, self.device.friendly_name,
+ self.device.model_name, self.device.manufacturer)
diff --git a/resources/pychromecast/pychromecast/authority_keys_pb2.py b/resources/pychromecast/pychromecast/authority_keys_pb2.py
new file mode 100644
index 0000000..226c3a7
--- /dev/null
+++ b/resources/pychromecast/pychromecast/authority_keys_pb2.py
@@ -0,0 +1,118 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: authority_keys.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='authority_keys.proto',
+ package='extensions.api.cast_channel.proto',
+ syntax='proto2',
+ serialized_pb=_b('\n\x14\x61uthority_keys.proto\x12!extensions.api.cast_channel.proto\"\x83\x01\n\rAuthorityKeys\x12\x42\n\x04keys\x18\x01 \x03(\x0b\x32\x34.extensions.api.cast_channel.proto.AuthorityKeys.Key\x1a.\n\x03Key\x12\x13\n\x0b\x66ingerprint\x18\x01 \x02(\x0c\x12\x12\n\npublic_key\x18\x02 \x02(\x0c\x42\x02H\x03')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_AUTHORITYKEYS_KEY = _descriptor.Descriptor(
+ name='Key',
+ full_name='extensions.api.cast_channel.proto.AuthorityKeys.Key',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='fingerprint', full_name='extensions.api.cast_channel.proto.AuthorityKeys.Key.fingerprint', index=0,
+ number=1, type=12, cpp_type=9, label=2,
+ has_default_value=False, default_value=_b(""),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='public_key', full_name='extensions.api.cast_channel.proto.AuthorityKeys.Key.public_key', index=1,
+ number=2, type=12, cpp_type=9, label=2,
+ has_default_value=False, default_value=_b(""),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=145,
+ serialized_end=191,
+)
+
+_AUTHORITYKEYS = _descriptor.Descriptor(
+ name='AuthorityKeys',
+ full_name='extensions.api.cast_channel.proto.AuthorityKeys',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='keys', full_name='extensions.api.cast_channel.proto.AuthorityKeys.keys', index=0,
+ number=1, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[_AUTHORITYKEYS_KEY, ],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=60,
+ serialized_end=191,
+)
+
+_AUTHORITYKEYS_KEY.containing_type = _AUTHORITYKEYS
+_AUTHORITYKEYS.fields_by_name['keys'].message_type = _AUTHORITYKEYS_KEY
+DESCRIPTOR.message_types_by_name['AuthorityKeys'] = _AUTHORITYKEYS
+
+AuthorityKeys = _reflection.GeneratedProtocolMessageType('AuthorityKeys', (_message.Message,), dict(
+
+ Key = _reflection.GeneratedProtocolMessageType('Key', (_message.Message,), dict(
+ DESCRIPTOR = _AUTHORITYKEYS_KEY,
+ __module__ = 'authority_keys_pb2'
+ # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.AuthorityKeys.Key)
+ ))
+ ,
+ DESCRIPTOR = _AUTHORITYKEYS,
+ __module__ = 'authority_keys_pb2'
+ # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.AuthorityKeys)
+ ))
+_sym_db.RegisterMessage(AuthorityKeys)
+_sym_db.RegisterMessage(AuthorityKeys.Key)
+
+
+DESCRIPTOR.has_options = True
+DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003'))
+# @@protoc_insertion_point(module_scope)
diff --git a/resources/pychromecast/pychromecast/cast_channel_pb2.py b/resources/pychromecast/pychromecast/cast_channel_pb2.py
new file mode 100644
index 0000000..eb3317f
--- /dev/null
+++ b/resources/pychromecast/pychromecast/cast_channel_pb2.py
@@ -0,0 +1,479 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: cast_channel.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='cast_channel.proto',
+ package='extensions.api.cast_channel',
+ syntax='proto2',
+ serialized_pb=_b('\n\x12\x63\x61st_channel.proto\x12\x1b\x65xtensions.api.cast_channel\"\xe3\x02\n\x0b\x43\x61stMessage\x12R\n\x10protocol_version\x18\x01 \x02(\x0e\x32\x38.extensions.api.cast_channel.CastMessage.ProtocolVersion\x12\x11\n\tsource_id\x18\x02 \x02(\t\x12\x16\n\x0e\x64\x65stination_id\x18\x03 \x02(\t\x12\x11\n\tnamespace\x18\x04 \x02(\t\x12J\n\x0cpayload_type\x18\x05 \x02(\x0e\x32\x34.extensions.api.cast_channel.CastMessage.PayloadType\x12\x14\n\x0cpayload_utf8\x18\x06 \x01(\t\x12\x16\n\x0epayload_binary\x18\x07 \x01(\x0c\"!\n\x0fProtocolVersion\x12\x0e\n\nCASTV2_1_0\x10\x00\"%\n\x0bPayloadType\x12\n\n\x06STRING\x10\x00\x12\n\n\x06\x42INARY\x10\x01\"\xce\x01\n\rAuthChallenge\x12]\n\x13signature_algorithm\x18\x01 \x01(\x0e\x32/.extensions.api.cast_channel.SignatureAlgorithm:\x0fRSASSA_PKCS1v15\x12\x14\n\x0csender_nonce\x18\x02 \x01(\x0c\x12H\n\x0ehash_algorithm\x18\x03 \x01(\x0e\x32*.extensions.api.cast_channel.HashAlgorithm:\x04SHA1\"\xb0\x02\n\x0c\x41uthResponse\x12\x11\n\tsignature\x18\x01 \x02(\x0c\x12\x1f\n\x17\x63lient_auth_certificate\x18\x02 \x02(\x0c\x12 \n\x18intermediate_certificate\x18\x03 \x03(\x0c\x12]\n\x13signature_algorithm\x18\x04 \x01(\x0e\x32/.extensions.api.cast_channel.SignatureAlgorithm:\x0fRSASSA_PKCS1v15\x12\x14\n\x0csender_nonce\x18\x05 \x01(\x0c\x12H\n\x0ehash_algorithm\x18\x06 \x01(\x0e\x32*.extensions.api.cast_channel.HashAlgorithm:\x04SHA1\x12\x0b\n\x03\x63rl\x18\x07 \x01(\x0c\"\xa3\x01\n\tAuthError\x12\x44\n\nerror_type\x18\x01 \x02(\x0e\x32\x30.extensions.api.cast_channel.AuthError.ErrorType\"P\n\tErrorType\x12\x12\n\x0eINTERNAL_ERROR\x10\x00\x12\n\n\x06NO_TLS\x10\x01\x12#\n\x1fSIGNATURE_ALGORITHM_UNAVAILABLE\x10\x02\"\xc6\x01\n\x11\x44\x65viceAuthMessage\x12=\n\tchallenge\x18\x01 \x01(\x0b\x32*.extensions.api.cast_channel.AuthChallenge\x12;\n\x08response\x18\x02 \x01(\x0b\x32).extensions.api.cast_channel.AuthResponse\x12\x35\n\x05\x65rror\x18\x03 \x01(\x0b\x32&.extensions.api.cast_channel.AuthError*J\n\x12SignatureAlgorithm\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x13\n\x0fRSASSA_PKCS1v15\x10\x01\x12\x0e\n\nRSASSA_PSS\x10\x02*%\n\rHashAlgorithm\x12\x08\n\x04SHA1\x10\x00\x12\n\n\x06SHA256\x10\x01\x42\x02H\x03')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+_SIGNATUREALGORITHM = _descriptor.EnumDescriptor(
+ name='SignatureAlgorithm',
+ full_name='extensions.api.cast_channel.SignatureAlgorithm',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='UNSPECIFIED', index=0, number=0,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='RSASSA_PKCS1v15', index=1, number=1,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='RSASSA_PSS', index=2, number=2,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=1292,
+ serialized_end=1366,
+)
+_sym_db.RegisterEnumDescriptor(_SIGNATUREALGORITHM)
+
+SignatureAlgorithm = enum_type_wrapper.EnumTypeWrapper(_SIGNATUREALGORITHM)
+_HASHALGORITHM = _descriptor.EnumDescriptor(
+ name='HashAlgorithm',
+ full_name='extensions.api.cast_channel.HashAlgorithm',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='SHA1', index=0, number=0,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SHA256', index=1, number=1,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=1368,
+ serialized_end=1405,
+)
+_sym_db.RegisterEnumDescriptor(_HASHALGORITHM)
+
+HashAlgorithm = enum_type_wrapper.EnumTypeWrapper(_HASHALGORITHM)
+UNSPECIFIED = 0
+RSASSA_PKCS1v15 = 1
+RSASSA_PSS = 2
+SHA1 = 0
+SHA256 = 1
+
+
+_CASTMESSAGE_PROTOCOLVERSION = _descriptor.EnumDescriptor(
+ name='ProtocolVersion',
+ full_name='extensions.api.cast_channel.CastMessage.ProtocolVersion',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='CASTV2_1_0', index=0, number=0,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=335,
+ serialized_end=368,
+)
+_sym_db.RegisterEnumDescriptor(_CASTMESSAGE_PROTOCOLVERSION)
+
+_CASTMESSAGE_PAYLOADTYPE = _descriptor.EnumDescriptor(
+ name='PayloadType',
+ full_name='extensions.api.cast_channel.CastMessage.PayloadType',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='STRING', index=0, number=0,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='BINARY', index=1, number=1,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=370,
+ serialized_end=407,
+)
+_sym_db.RegisterEnumDescriptor(_CASTMESSAGE_PAYLOADTYPE)
+
+_AUTHERROR_ERRORTYPE = _descriptor.EnumDescriptor(
+ name='ErrorType',
+ full_name='extensions.api.cast_channel.AuthError.ErrorType',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='INTERNAL_ERROR', index=0, number=0,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='NO_TLS', index=1, number=1,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SIGNATURE_ALGORITHM_UNAVAILABLE', index=2, number=2,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=1009,
+ serialized_end=1089,
+)
+_sym_db.RegisterEnumDescriptor(_AUTHERROR_ERRORTYPE)
+
+
+_CASTMESSAGE = _descriptor.Descriptor(
+ name='CastMessage',
+ full_name='extensions.api.cast_channel.CastMessage',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='protocol_version', full_name='extensions.api.cast_channel.CastMessage.protocol_version', index=0,
+ number=1, type=14, cpp_type=8, label=2,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='source_id', full_name='extensions.api.cast_channel.CastMessage.source_id', index=1,
+ number=2, type=9, cpp_type=9, label=2,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='destination_id', full_name='extensions.api.cast_channel.CastMessage.destination_id', index=2,
+ number=3, type=9, cpp_type=9, label=2,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='namespace', full_name='extensions.api.cast_channel.CastMessage.namespace', index=3,
+ number=4, type=9, cpp_type=9, label=2,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='payload_type', full_name='extensions.api.cast_channel.CastMessage.payload_type', index=4,
+ number=5, type=14, cpp_type=8, label=2,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='payload_utf8', full_name='extensions.api.cast_channel.CastMessage.payload_utf8', index=5,
+ number=6, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='payload_binary', full_name='extensions.api.cast_channel.CastMessage.payload_binary', index=6,
+ number=7, type=12, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b(""),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _CASTMESSAGE_PROTOCOLVERSION,
+ _CASTMESSAGE_PAYLOADTYPE,
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=52,
+ serialized_end=407,
+)
+
+
+_AUTHCHALLENGE = _descriptor.Descriptor(
+ name='AuthChallenge',
+ full_name='extensions.api.cast_channel.AuthChallenge',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='signature_algorithm', full_name='extensions.api.cast_channel.AuthChallenge.signature_algorithm', index=0,
+ number=1, type=14, cpp_type=8, label=1,
+ has_default_value=True, default_value=1,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='sender_nonce', full_name='extensions.api.cast_channel.AuthChallenge.sender_nonce', index=1,
+ number=2, type=12, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b(""),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='hash_algorithm', full_name='extensions.api.cast_channel.AuthChallenge.hash_algorithm', index=2,
+ number=3, type=14, cpp_type=8, label=1,
+ has_default_value=True, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=410,
+ serialized_end=616,
+)
+
+
+_AUTHRESPONSE = _descriptor.Descriptor(
+ name='AuthResponse',
+ full_name='extensions.api.cast_channel.AuthResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='signature', full_name='extensions.api.cast_channel.AuthResponse.signature', index=0,
+ number=1, type=12, cpp_type=9, label=2,
+ has_default_value=False, default_value=_b(""),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='client_auth_certificate', full_name='extensions.api.cast_channel.AuthResponse.client_auth_certificate', index=1,
+ number=2, type=12, cpp_type=9, label=2,
+ has_default_value=False, default_value=_b(""),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='intermediate_certificate', full_name='extensions.api.cast_channel.AuthResponse.intermediate_certificate', index=2,
+ number=3, type=12, cpp_type=9, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='signature_algorithm', full_name='extensions.api.cast_channel.AuthResponse.signature_algorithm', index=3,
+ number=4, type=14, cpp_type=8, label=1,
+ has_default_value=True, default_value=1,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='sender_nonce', full_name='extensions.api.cast_channel.AuthResponse.sender_nonce', index=4,
+ number=5, type=12, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b(""),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='hash_algorithm', full_name='extensions.api.cast_channel.AuthResponse.hash_algorithm', index=5,
+ number=6, type=14, cpp_type=8, label=1,
+ has_default_value=True, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='crl', full_name='extensions.api.cast_channel.AuthResponse.crl', index=6,
+ number=7, type=12, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b(""),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=619,
+ serialized_end=923,
+)
+
+
+_AUTHERROR = _descriptor.Descriptor(
+ name='AuthError',
+ full_name='extensions.api.cast_channel.AuthError',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='error_type', full_name='extensions.api.cast_channel.AuthError.error_type', index=0,
+ number=1, type=14, cpp_type=8, label=2,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _AUTHERROR_ERRORTYPE,
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=926,
+ serialized_end=1089,
+)
+
+
+_DEVICEAUTHMESSAGE = _descriptor.Descriptor(
+ name='DeviceAuthMessage',
+ full_name='extensions.api.cast_channel.DeviceAuthMessage',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='challenge', full_name='extensions.api.cast_channel.DeviceAuthMessage.challenge', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='response', full_name='extensions.api.cast_channel.DeviceAuthMessage.response', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='error', full_name='extensions.api.cast_channel.DeviceAuthMessage.error', index=2,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1092,
+ serialized_end=1290,
+)
+
+_CASTMESSAGE.fields_by_name['protocol_version'].enum_type = _CASTMESSAGE_PROTOCOLVERSION
+_CASTMESSAGE.fields_by_name['payload_type'].enum_type = _CASTMESSAGE_PAYLOADTYPE
+_CASTMESSAGE_PROTOCOLVERSION.containing_type = _CASTMESSAGE
+_CASTMESSAGE_PAYLOADTYPE.containing_type = _CASTMESSAGE
+_AUTHCHALLENGE.fields_by_name['signature_algorithm'].enum_type = _SIGNATUREALGORITHM
+_AUTHCHALLENGE.fields_by_name['hash_algorithm'].enum_type = _HASHALGORITHM
+_AUTHRESPONSE.fields_by_name['signature_algorithm'].enum_type = _SIGNATUREALGORITHM
+_AUTHRESPONSE.fields_by_name['hash_algorithm'].enum_type = _HASHALGORITHM
+_AUTHERROR.fields_by_name['error_type'].enum_type = _AUTHERROR_ERRORTYPE
+_AUTHERROR_ERRORTYPE.containing_type = _AUTHERROR
+_DEVICEAUTHMESSAGE.fields_by_name['challenge'].message_type = _AUTHCHALLENGE
+_DEVICEAUTHMESSAGE.fields_by_name['response'].message_type = _AUTHRESPONSE
+_DEVICEAUTHMESSAGE.fields_by_name['error'].message_type = _AUTHERROR
+DESCRIPTOR.message_types_by_name['CastMessage'] = _CASTMESSAGE
+DESCRIPTOR.message_types_by_name['AuthChallenge'] = _AUTHCHALLENGE
+DESCRIPTOR.message_types_by_name['AuthResponse'] = _AUTHRESPONSE
+DESCRIPTOR.message_types_by_name['AuthError'] = _AUTHERROR
+DESCRIPTOR.message_types_by_name['DeviceAuthMessage'] = _DEVICEAUTHMESSAGE
+DESCRIPTOR.enum_types_by_name['SignatureAlgorithm'] = _SIGNATUREALGORITHM
+DESCRIPTOR.enum_types_by_name['HashAlgorithm'] = _HASHALGORITHM
+
+CastMessage = _reflection.GeneratedProtocolMessageType('CastMessage', (_message.Message,), dict(
+ DESCRIPTOR = _CASTMESSAGE,
+ __module__ = 'cast_channel_pb2'
+ # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.CastMessage)
+ ))
+_sym_db.RegisterMessage(CastMessage)
+
+AuthChallenge = _reflection.GeneratedProtocolMessageType('AuthChallenge', (_message.Message,), dict(
+ DESCRIPTOR = _AUTHCHALLENGE,
+ __module__ = 'cast_channel_pb2'
+ # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthChallenge)
+ ))
+_sym_db.RegisterMessage(AuthChallenge)
+
+AuthResponse = _reflection.GeneratedProtocolMessageType('AuthResponse', (_message.Message,), dict(
+ DESCRIPTOR = _AUTHRESPONSE,
+ __module__ = 'cast_channel_pb2'
+ # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthResponse)
+ ))
+_sym_db.RegisterMessage(AuthResponse)
+
+AuthError = _reflection.GeneratedProtocolMessageType('AuthError', (_message.Message,), dict(
+ DESCRIPTOR = _AUTHERROR,
+ __module__ = 'cast_channel_pb2'
+ # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthError)
+ ))
+_sym_db.RegisterMessage(AuthError)
+
+DeviceAuthMessage = _reflection.GeneratedProtocolMessageType('DeviceAuthMessage', (_message.Message,), dict(
+ DESCRIPTOR = _DEVICEAUTHMESSAGE,
+ __module__ = 'cast_channel_pb2'
+ # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.DeviceAuthMessage)
+ ))
+_sym_db.RegisterMessage(DeviceAuthMessage)
+
+
+DESCRIPTOR.has_options = True
+DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003'))
+# @@protoc_insertion_point(module_scope)
diff --git a/resources/pychromecast/pychromecast/config.py b/resources/pychromecast/pychromecast/config.py
new file mode 100644
index 0000000..73a6d83
--- /dev/null
+++ b/resources/pychromecast/pychromecast/config.py
@@ -0,0 +1,43 @@
+"""
+Data and methods to retrieve app specific configuration
+"""
+import json
+
+import requests
+
+APP_BACKDROP = "E8C28D3C"
+APP_YOUTUBE = "YouTube"
+APP_MEDIA_RECEIVER = "CC1AD845"
+APP_PLEX = "06ee44ee-e7e3-4249-83b6-f5d0b6f07f34_1"
+APP_DASHCAST = "84912283"
+APP_SPOTIFY = "CC32E753"
+
+
+def get_possible_app_ids():
+ """ Returns all possible app ids. """
+
+ try:
+ req = requests.get(
+ "https://clients3.google.com/cast/chromecast/device/baseconfig")
+ data = json.loads(req.text[4:])
+
+ return [app['app_id'] for app in data['applications']] + \
+ data["enabled_app_ids"]
+
+ except ValueError:
+ # If json fails to parse
+ return []
+
+
+def get_app_config(app_id):
+ """ Get specific configuration for 'app_id'. """
+ try:
+ req = requests.get(
+ ("https://clients3.google.com/"
+ "cast/chromecast/device/app?a={}").format(app_id))
+
+ return json.loads(req.text[4:]) if req.status_code == 200 else {}
+
+ except ValueError:
+ # If json fails to parse
+ return {}
diff --git a/resources/pychromecast/pychromecast/controllers/__init__.py b/resources/pychromecast/pychromecast/controllers/__init__.py
new file mode 100644
index 0000000..718cd13
--- /dev/null
+++ b/resources/pychromecast/pychromecast/controllers/__init__.py
@@ -0,0 +1,104 @@
+"""
+Provides controllers to handle specific namespaces in Chromecast communication.
+"""
+import logging
+
+from ..error import UnsupportedNamespace, ControllerNotRegistered
+
+
+class BaseController(object):
+ """ ABC for namespace controllers. """
+
+ def __init__(self, namespace, supporting_app_id=None,
+ target_platform=False):
+ """
+ Initialize the controller.
+
+ namespace: the namespace this controller will act on
+ supporting_app_id: app to be launched if app is running with
+ unsupported namespace.
+ target_platform: set to True if you target the platform instead of
+ current app.
+ """
+ self.namespace = namespace
+ self.supporting_app_id = supporting_app_id
+ self.target_platform = target_platform
+
+ self._socket_client = None
+ self._message_func = None
+
+ self.logger = logging.getLogger(__name__)
+
+ @property
+ def is_active(self):
+ """ True if the controller is connected to a socket client and the
+ Chromecast is running an app that supports this controller. """
+ return (self._socket_client is not None and
+ self.namespace in self._socket_client.app_namespaces)
+
+ def launch(self, callback_function=None):
+ """ If set, launches app related to the controller. """
+ self._check_registered()
+
+ self._socket_client.receiver_controller.launch_app(
+ self.supporting_app_id, callback_function=callback_function)
+
+ def registered(self, socket_client):
+ """ Called when a controller is registered. """
+ self._socket_client = socket_client
+
+ if self.target_platform:
+ self._message_func = self._socket_client.send_platform_message
+ else:
+ self._message_func = self._socket_client.send_app_message
+
+ def channel_connected(self):
+ """ Called when a channel has been openend that supports the
+ namespace of this controller. """
+ pass
+
+ def channel_disconnected(self):
+ """ Called when a channel is disconnected. """
+ pass
+
+ def send_message(self, data, inc_session_id=False,
+ callback_function=None):
+ """
+ Send a message on this namespace to the Chromecast.
+
+ Will raise a NotConnected exception if not connected.
+ """
+ self._check_registered()
+
+ if not self.target_platform and \
+ self.namespace not in self._socket_client.app_namespaces:
+ if self.supporting_app_id is not None:
+ self.launch()
+
+ else:
+ raise UnsupportedNamespace(
+ ("Namespace {} is not supported by running"
+ "application.").format(self.namespace))
+
+ return self._message_func(
+ self.namespace, data, inc_session_id, callback_function)
+
+ # pylint: disable=unused-argument,no-self-use
+ def receive_message(self, message, data):
+ """
+ Called when a message is received that matches the namespace.
+ Returns boolean indicating if message was handled.
+ """
+ return False
+
+ def tear_down(self):
+ """ Called when we are shutting down. """
+ self._socket_client = None
+ self._message_func = None
+
+ def _check_registered(self):
+ """ Helper method to see if we are registered with a Cast object. """
+ if self._socket_client is None:
+ raise ControllerNotRegistered((
+ "Trying to use the controller without it being registered "
+ "with a Cast object."))
diff --git a/resources/pychromecast/pychromecast/controllers/dashcast.py b/resources/pychromecast/pychromecast/controllers/dashcast.py
new file mode 100644
index 0000000..bdab6ee
--- /dev/null
+++ b/resources/pychromecast/pychromecast/controllers/dashcast.py
@@ -0,0 +1,62 @@
+"""
+Controller to interface with the DashCast app namespace.
+"""
+from ..config import APP_DASHCAST
+from . import BaseController
+
+
+APP_NAMESPACE = "urn:x-cast:com.madmod.dashcast"
+
+
+class DashCastController(BaseController):
+ """ Controller to interact with DashCast app namespace. """
+
+ # pylint: disable=useless-super-delegation
+ # The pylint rule useless-super-delegation doesn't realize
+ # we are setting default values here.
+ def __init__(self,
+ appNamespace=APP_NAMESPACE,
+ appId=APP_DASHCAST):
+ super(DashCastController, self).__init__(
+ appNamespace, appId)
+ # pylint: enable=useless-super-delegation
+
+ def receive_message(self, message, data):
+ """
+ Called when a load complete message is received.
+
+ This is currently un-used by this controller. It is implemented
+ so that we don't get "Message unhandled" warnings. In the future
+ it might be used to update a public status object like the media
+ controller does.
+ """
+ # Indicate that the message was successfully handled.
+ return True
+
+ def load_url(self, url, force=False, reload_seconds=0,
+ callback_function=None):
+ """
+ Starts loading a URL with an optional reload time
+ in seconds.
+
+ Setting force to True may load pages which block
+ iframe embedding, but will prevent reload from
+ working and will cause calls to load_url()
+ to reload the app.
+ """
+ def launch_callback():
+ """Loads requested URL after app launched."""
+ should_reload = not force and reload_seconds not in (0, None)
+ reload_milliseconds = (0 if not should_reload
+ else reload_seconds * 1000)
+ msg = {
+ "url": url,
+ "force": force,
+ "reload": should_reload,
+ "reload_time": reload_milliseconds
+ }
+
+ self.send_message(msg, inc_session_id=True,
+ callback_function=callback_function)
+
+ self.launch(callback_function=launch_callback)
diff --git a/resources/pychromecast/pychromecast/controllers/media.py b/resources/pychromecast/pychromecast/controllers/media.py
new file mode 100644
index 0000000..99f54eb
--- /dev/null
+++ b/resources/pychromecast/pychromecast/controllers/media.py
@@ -0,0 +1,535 @@
+"""
+Provides a controller for controlling the default media players
+on the Chromecast.
+"""
+from datetime import datetime
+
+from collections import namedtuple
+import threading
+
+from ..config import APP_MEDIA_RECEIVER
+from . import BaseController
+
+STREAM_TYPE_UNKNOWN = "UNKNOWN"
+STREAM_TYPE_BUFFERED = "BUFFERED"
+STREAM_TYPE_LIVE = "LIVE"
+
+MEDIA_PLAYER_STATE_PLAYING = "PLAYING"
+MEDIA_PLAYER_STATE_BUFFERING = "BUFFERING"
+MEDIA_PLAYER_STATE_PAUSED = "PAUSED"
+MEDIA_PLAYER_STATE_IDLE = "IDLE"
+MEDIA_PLAYER_STATE_UNKNOWN = "UNKNOWN"
+
+MESSAGE_TYPE = 'type'
+
+TYPE_GET_STATUS = "GET_STATUS"
+TYPE_MEDIA_STATUS = "MEDIA_STATUS"
+TYPE_PLAY = "PLAY"
+TYPE_PAUSE = "PAUSE"
+TYPE_STOP = "STOP"
+TYPE_LOAD = "LOAD"
+TYPE_SEEK = "SEEK"
+TYPE_EDIT_TRACKS_INFO = "EDIT_TRACKS_INFO"
+
+METADATA_TYPE_GENERIC = 0
+METADATA_TYPE_TVSHOW = 1
+METADATA_TYPE_MOVIE = 2
+METADATA_TYPE_MUSICTRACK = 3
+METADATA_TYPE_PHOTO = 4
+
+CMD_SUPPORT_PAUSE = 1
+CMD_SUPPORT_SEEK = 2
+CMD_SUPPORT_STREAM_VOLUME = 4
+CMD_SUPPORT_STREAM_MUTE = 8
+CMD_SUPPORT_SKIP_FORWARD = 16
+CMD_SUPPORT_SKIP_BACKWARD = 32
+
+
+MediaImage = namedtuple('MediaImage', 'url height width')
+
+
+class MediaStatus(object):
+ """ Class to hold the media status. """
+
+ # pylint: disable=too-many-instance-attributes,too-many-public-methods
+ def __init__(self):
+ self.current_time = 0
+ self.content_id = None
+ self.content_type = None
+ self.duration = None
+ self.stream_type = STREAM_TYPE_UNKNOWN
+ self.idle_reason = None
+ self.media_session_id = None
+ self.playback_rate = 1
+ self.player_state = MEDIA_PLAYER_STATE_UNKNOWN
+ self.supported_media_commands = 0
+ self.volume_level = 1
+ self.volume_muted = False
+ self.media_custom_data = {}
+ self.media_metadata = {}
+ self.subtitle_tracks = {}
+ self.current_subtitle_tracks = []
+ self.last_updated = None
+
+ @property
+ def adjusted_current_time(self):
+ """ Returns calculated current seek time of media in seconds """
+ if self.player_state == MEDIA_PLAYER_STATE_PLAYING:
+ # Add time since last update
+ return (self.current_time +
+ (datetime.utcnow() - self.last_updated).total_seconds())
+ # Not playing, return last reported seek time
+ return self.current_time
+
+ @property
+ def metadata_type(self):
+ """ Type of meta data. """
+ return self.media_metadata.get('metadataType')
+
+ @property
+ def player_is_playing(self):
+ """ Return True if player is PLAYING. """
+ return (self.player_state == MEDIA_PLAYER_STATE_PLAYING or
+ self.player_state == MEDIA_PLAYER_STATE_BUFFERING)
+
+ @property
+ def player_is_paused(self):
+ """ Return True if player is PAUSED. """
+ return self.player_state == MEDIA_PLAYER_STATE_PAUSED
+
+ @property
+ def player_is_idle(self):
+ """ Return True if player is IDLE. """
+ return self.player_state == MEDIA_PLAYER_STATE_IDLE
+
+ @property
+ def media_is_generic(self):
+ """ Return True if media status represents generic media. """
+ return self.metadata_type == METADATA_TYPE_GENERIC
+
+ @property
+ def media_is_tvshow(self):
+ """ Return True if media status represents a tv show. """
+ return self.metadata_type == METADATA_TYPE_TVSHOW
+
+ @property
+ def media_is_movie(self):
+ """ Return True if media status represents a movie. """
+ return self.metadata_type == METADATA_TYPE_MOVIE
+
+ @property
+ def media_is_musictrack(self):
+ """ Return True if media status represents a musictrack. """
+ return self.metadata_type == METADATA_TYPE_MUSICTRACK
+
+ @property
+ def media_is_photo(self):
+ """ Return True if media status represents a photo. """
+ return self.metadata_type == METADATA_TYPE_PHOTO
+
+ @property
+ def stream_type_is_buffered(self):
+ """ Return True if stream type is BUFFERED. """
+ return self.stream_type == STREAM_TYPE_BUFFERED
+
+ @property
+ def stream_type_is_live(self):
+ """ Return True if stream type is LIVE. """
+ return self.stream_type == STREAM_TYPE_LIVE
+
+ @property
+ def title(self):
+ """ Return title of media. """
+ return self.media_metadata.get('title')
+
+ @property
+ def series_title(self):
+ """ Return series title if available. """
+ return self.media_metadata.get('seriesTitle')
+
+ @property
+ def season(self):
+ """ Return season if available. """
+ return self.media_metadata.get('season')
+
+ @property
+ def episode(self):
+ """ Return episode if available. """
+ return self.media_metadata.get('episode')
+
+ @property
+ def artist(self):
+ """ Return artist if available. """
+ return self.media_metadata.get('artist')
+
+ @property
+ def album_name(self):
+ """ Return album name if available. """
+ return self.media_metadata.get('albumName')
+
+ @property
+ def album_artist(self):
+ """ Return album artist if available. """
+ return self.media_metadata.get('albumArtist')
+
+ @property
+ def track(self):
+ """ Return track number if available. """
+ return self.media_metadata.get('track')
+
+ @property
+ def images(self):
+ """ Return a list of MediaImage objects for this media. """
+ return [
+ MediaImage(item.get('url'), item.get('height'), item.get('width'))
+ for item in self.media_metadata.get('images', [])
+ ]
+
+ @property
+ def supports_pause(self):
+ """ True if PAUSE is supported. """
+ return bool(self.supported_media_commands & CMD_SUPPORT_PAUSE)
+
+ @property
+ def supports_seek(self):
+ """ True if SEEK is supported. """
+ return bool(self.supported_media_commands & CMD_SUPPORT_SEEK)
+
+ @property
+ def supports_stream_volume(self):
+ """ True if STREAM_VOLUME is supported. """
+ return bool(self.supported_media_commands & CMD_SUPPORT_STREAM_VOLUME)
+
+ @property
+ def supports_stream_mute(self):
+ """ True if STREAM_MUTE is supported. """
+ return bool(self.supported_media_commands & CMD_SUPPORT_STREAM_MUTE)
+
+ @property
+ def supports_skip_forward(self):
+ """ True if SKIP_FORWARD is supported. """
+ return bool(self.supported_media_commands & CMD_SUPPORT_SKIP_FORWARD)
+
+ @property
+ def supports_skip_backward(self):
+ """ True if SKIP_BACKWARD is supported. """
+ return bool(self.supported_media_commands & CMD_SUPPORT_SKIP_BACKWARD)
+
+ def update(self, data):
+ """ New data will only contain the changed attributes. """
+ if not data.get('status', []):
+ return
+
+ status_data = data['status'][0]
+ media_data = status_data.get('media') or {}
+ volume_data = status_data.get('volume', {})
+
+ self.current_time = status_data.get('currentTime', self.current_time)
+ self.content_id = media_data.get('contentId', self.content_id)
+ self.content_type = media_data.get('contentType', self.content_type)
+ self.duration = media_data.get('duration', self.duration)
+ self.stream_type = media_data.get('streamType', self.stream_type)
+ self.idle_reason = status_data.get('idleReason', self.idle_reason)
+ self.media_session_id = status_data.get(
+ 'mediaSessionId', self.media_session_id)
+ self.playback_rate = status_data.get(
+ 'playbackRate', self.playback_rate)
+ self.player_state = status_data.get('playerState', self.player_state)
+ self.supported_media_commands = status_data.get(
+ 'supportedMediaCommands', self.supported_media_commands)
+ self.volume_level = volume_data.get('level', self.volume_level)
+ self.volume_muted = volume_data.get('muted', self.volume_muted)
+ self.media_custom_data = media_data.get(
+ 'customData', self.media_custom_data)
+ self.media_metadata = media_data.get('metadata', self.media_metadata)
+ self.subtitle_tracks = media_data.get('tracks', self.subtitle_tracks)
+ self.current_subtitle_tracks = status_data.get(
+ 'activeTrackIds', self.current_subtitle_tracks)
+ self.last_updated = datetime.utcnow()
+
+ def __repr__(self):
+ info = {
+ 'metadata_type': self.metadata_type,
+ 'title': self.title,
+ 'series_title': self.series_title,
+ 'season': self.season,
+ 'episode': self.episode,
+ 'artist': self.artist,
+ 'album_name': self.album_name,
+ 'album_artist': self.album_artist,
+ 'track': self.track,
+ 'subtitle_tracks': self.subtitle_tracks,
+ 'images': self.images,
+ 'supports_pause': self.supports_pause,
+ 'supports_seek': self.supports_seek,
+ 'supports_stream_volume': self.supports_stream_volume,
+ 'supports_stream_mute': self.supports_stream_mute,
+ 'supports_skip_forward': self.supports_skip_forward,
+ 'supports_skip_backward': self.supports_skip_backward,
+ }
+ info.update(self.__dict__)
+ return ''.format(info)
+
+
+# pylint: disable=too-many-public-methods
+class MediaController(BaseController):
+ """ Controller to interact with Google media namespace. """
+
+ def __init__(self):
+ super(MediaController, self).__init__(
+ "urn:x-cast:com.google.cast.media")
+
+ self.media_session_id = 0
+ self.status = MediaStatus()
+ self.session_active_event = threading.Event()
+ self.app_id = APP_MEDIA_RECEIVER
+ self._status_listeners = []
+
+ def channel_connected(self):
+ """ Called when media channel is connected. Will update status. """
+ self.update_status()
+
+ def channel_disconnected(self):
+ """ Called when a media channel is disconnected. Will erase status. """
+ self.status = MediaStatus()
+ self._fire_status_changed()
+
+ def receive_message(self, message, data):
+ """ Called when a media message is received. """
+ if data[MESSAGE_TYPE] == TYPE_MEDIA_STATUS:
+ self._process_media_status(data)
+
+ return True
+
+ return False
+
+ def register_status_listener(self, listener):
+ """ Register a listener for new media statusses. A new status will
+ call listener.new_media_status(status) """
+ self._status_listeners.append(listener)
+
+ def update_status(self, callback_function_param=False):
+ """ Send message to update the status. """
+ self.send_message({MESSAGE_TYPE: TYPE_GET_STATUS},
+ callback_function=callback_function_param)
+
+ def _send_command(self, command):
+ """ Send a command to the Chromecast on media channel. """
+ if self.status is None or self.status.media_session_id is None:
+ self.logger.warning(
+ "%s command requested but no session is active.",
+ command[MESSAGE_TYPE])
+ return
+
+ command['mediaSessionId'] = self.status.media_session_id
+
+ self.send_message(command, inc_session_id=True)
+
+ @property
+ def is_playing(self):
+ """ Deprecated as of June 8, 2015. Use self.status.player_is_playing.
+ Returns if the Chromecast is playing. """
+ return self.status is not None and self.status.player_is_playing
+
+ @property
+ def is_paused(self):
+ """ Deprecated as of June 8, 2015. Use self.status.player_is_paused.
+ Returns if the Chromecast is paused. """
+ return self.status is not None and self.status.player_is_paused
+
+ @property
+ def is_idle(self):
+ """ Deprecated as of June 8, 2015. Use self.status.player_is_idle.
+ Returns if the Chromecast is idle on a media supported app. """
+ return self.status is not None and self.status.player_is_idle
+
+ @property
+ def title(self):
+ """ Deprecated as of June 8, 2015. Use self.status.title.
+ Return title of the current playing item. """
+ return None if not self.status else self.status.title
+
+ @property
+ def thumbnail(self):
+ """ Deprecated as of June 8, 2015. Use self.status.images.
+ Return thumbnail url of current playing item. """
+ if not self.status:
+ return None
+
+ images = self.status.images
+
+ return images[0].url if images else None
+
+ def play(self):
+ """ Send the PLAY command. """
+ self._send_command({MESSAGE_TYPE: TYPE_PLAY})
+
+ def pause(self):
+ """ Send the PAUSE command. """
+ self._send_command({MESSAGE_TYPE: TYPE_PAUSE})
+
+ def stop(self):
+ """ Send the STOP command. """
+ self._send_command({MESSAGE_TYPE: TYPE_STOP})
+
+ def rewind(self):
+ """ Starts playing the media from the beginning. """
+ self.seek(0)
+
+ def skip(self):
+ """ Skips rest of the media. Values less then -5 behaved flaky. """
+ self.seek(int(self.status.duration)-5)
+
+ def seek(self, position):
+ """ Seek the media to a specific location. """
+ self._send_command({MESSAGE_TYPE: TYPE_SEEK,
+ "currentTime": position,
+ "resumeState": "PLAYBACK_START"})
+
+ def enable_subtitle(self, track_id):
+ """ Enable specific text track. """
+ self._send_command({
+ MESSAGE_TYPE: TYPE_EDIT_TRACKS_INFO,
+ "activeTrackIds": [track_id]
+ })
+
+ def disable_subtitle(self):
+ """ Disable subtitle. """
+ self._send_command({
+ MESSAGE_TYPE: TYPE_EDIT_TRACKS_INFO,
+ "activeTrackIds": []
+ })
+
+ def block_until_active(self, timeout=None):
+ """
+ Blocks thread until the media controller session is active on the
+ chromecast. The media controller only accepts playback control
+ commands when a media session is active.
+
+ If a session is already active then the method returns immediately.
+
+ :param timeout: a floating point number specifying a timeout for the
+ operation in seconds (or fractions thereof). Or None
+ to block forever.
+ """
+ self.session_active_event.wait(timeout=timeout)
+
+ def _process_media_status(self, data):
+ """ Processes a STATUS message. """
+ self.status.update(data)
+
+ self.logger.debug("Media:Received status %s", data)
+
+ # Update session active threading event
+ if self.status.media_session_id is None:
+ self.session_active_event.clear()
+ else:
+ self.session_active_event.set()
+
+ self._fire_status_changed()
+
+ def _fire_status_changed(self):
+ """ Tells listeners of a changed status. """
+ for listener in self._status_listeners:
+ try:
+ listener.new_media_status(self.status)
+ except Exception: # pylint: disable=broad-except
+ pass
+
+ # pylint: disable=too-many-arguments
+ def play_media(self, url, content_type, title=None, thumb=None,
+ current_time=0, autoplay=True,
+ stream_type=STREAM_TYPE_BUFFERED,
+ metadata=None, subtitles=None, subtitles_lang='en-US',
+ subtitles_mime='text/vtt', subtitle_id=1):
+ """
+ Plays media on the Chromecast. Start default media receiver if not
+ already started.
+
+ Parameters:
+ url: str - url of the media.
+ content_type: str - mime type. Example: 'video/mp4'.
+ title: str - title of the media.
+ thumb: str - thumbnail image url.
+ current_time: float - seconds from the beginning of the media
+ to start playback.
+ autoplay: bool - whether the media will automatically play.
+ stream_type: str - describes the type of media artifact as one of the
+ following: "NONE", "BUFFERED", "LIVE".
+ subtitles: str - url of subtitle file to be shown on chromecast.
+ subtitles_lang: str - language for subtitles.
+ subtitles_mime: str - mimetype of subtitles.
+ subtitle_id: int - id of subtitle to be loaded.
+ metadata: dict - media metadata object, one of the following:
+ GenericMediaMetadata, MovieMediaMetadata, TvShowMediaMetadata,
+ MusicTrackMediaMetadata, PhotoMediaMetadata.
+
+ Docs:
+ https://developers.google.com/cast/docs/reference/messages#MediaData
+ """
+ # pylint: disable=too-many-locals
+ def app_launched_callback():
+ """Plays media after chromecast has switched to requested app."""
+ self._send_start_play_media(
+ url, content_type, title, thumb, current_time, autoplay,
+ stream_type, metadata, subtitles, subtitles_lang,
+ subtitles_mime, subtitle_id)
+
+ receiver_ctrl = self._socket_client.receiver_controller
+ receiver_ctrl.launch_app(self.app_id,
+ callback_function=app_launched_callback)
+
+ def _send_start_play_media(self, url, content_type, title=None, thumb=None,
+ current_time=0, autoplay=True,
+ stream_type=STREAM_TYPE_BUFFERED,
+ metadata=None, subtitles=None,
+ subtitles_lang='en-US',
+ subtitles_mime='text/vtt', subtitle_id=1):
+ # pylint: disable=too-many-locals
+ msg = {
+ 'media': {
+ 'contentId': url,
+ 'streamType': stream_type,
+ 'contentType': content_type,
+ 'metadata': metadata or {}
+ },
+ MESSAGE_TYPE: TYPE_LOAD,
+ 'currentTime': current_time,
+ 'autoplay': autoplay,
+ 'customData': {}
+ }
+
+ if title:
+ msg['media']['metadata']['title'] = title
+
+ if thumb:
+ msg['media']['metadata']['thumb'] = thumb
+
+ if 'images' not in msg['media']['metadata']:
+ msg['media']['metadata']['images'] = []
+
+ msg['media']['metadata']['images'].append({'url': thumb})
+ if subtitles:
+ sub_msg = [{
+ 'trackId': subtitle_id,
+ 'trackContentId': subtitles,
+ 'language': subtitles_lang,
+ 'subtype': 'SUBTITLES',
+ 'type': 'TEXT',
+ 'trackContentType': subtitles_mime,
+ 'name': "{} - {} Subtitle".format(subtitles_lang, subtitle_id)
+ }]
+ msg['media']['tracks'] = sub_msg
+ msg['media']['textTrackStyle'] = {
+ 'backgroundColor': '#FFFFFF00',
+ 'edgeType': 'OUTLINE',
+ 'edgeColor': '#000000FF'
+ }
+ msg['activeTrackIds'] = [subtitle_id]
+ self.send_message(msg, inc_session_id=True)
+
+ def tear_down(self):
+ """ Called when controller is destroyed. """
+ super(MediaController, self).tear_down()
+
+ self._status_listeners[:] = []
diff --git a/resources/pychromecast/pychromecast/controllers/plex.py b/resources/pychromecast/pychromecast/controllers/plex.py
new file mode 100644
index 0000000..b77ba67
--- /dev/null
+++ b/resources/pychromecast/pychromecast/controllers/plex.py
@@ -0,0 +1,30 @@
+"""
+Controller to interface with the Plex-app.
+"""
+from . import BaseController
+
+MESSAGE_TYPE = 'type'
+
+TYPE_PLAY = "PLAY"
+TYPE_PAUSE = "PAUSE"
+TYPE_STOP = "STOP"
+
+
+class PlexController(BaseController):
+ """ Controller to interact with Plex namespace. """
+
+ def __init__(self):
+ super(PlexController, self).__init__(
+ "urn:x-cast:plex", "9AC194DC")
+
+ def stop(self):
+ """ Send stop command. """
+ self.send_message({MESSAGE_TYPE: TYPE_STOP})
+
+ def pause(self):
+ """ Send pause command. """
+ self.send_message({MESSAGE_TYPE: TYPE_PAUSE})
+
+ def play(self):
+ """ Send play command. """
+ self.send_message({MESSAGE_TYPE: TYPE_PLAY})
diff --git a/resources/pychromecast/pychromecast/controllers/spotify.py b/resources/pychromecast/pychromecast/controllers/spotify.py
new file mode 100644
index 0000000..4abba4e
--- /dev/null
+++ b/resources/pychromecast/pychromecast/controllers/spotify.py
@@ -0,0 +1,50 @@
+"""
+Controller to interface with the DashCast app namespace.
+"""
+import logging
+import time
+
+from . import BaseController
+from ..config import APP_SPOTIFY
+
+APP_NAMESPACE = "urn:x-cast:com.spotify.chromecast.secure.v1"
+TYPE_STATUS = "setCredentials"
+TYPE_RESPONSE_STATUS = 'setCredentialsResponse'
+
+
+# pylint: disable=too-many-instance-attributes
+class SpotifyController(BaseController):
+ """ Controller to interact with Spotify namespace. """
+
+ # pylint: disable=useless-super-delegation
+ # The pylint rule useless-super-delegation doesn't realize
+ # we are setting default values here.
+ def __init__(self, access_token):
+ super(SpotifyController, self).__init__(APP_NAMESPACE, APP_SPOTIFY)
+
+ self.logger = logging.getLogger(__name__)
+ self.session_started = False
+ self.access_token = access_token
+ self.is_launched = False
+ # pylint: enable=useless-super-delegation
+
+ # pylint: disable=unused-argument,no-self-use
+ def receive_message(self, message, data):
+ """ Currently not doing anything with received messages. """
+ if data['type'] == TYPE_RESPONSE_STATUS:
+ self.is_launched = True
+ return True
+
+ def launch_app(self):
+ """ Launch main application """
+
+ def callback():
+ """Callback function"""
+ self.send_message({"type": TYPE_STATUS,
+ "credentials": self.access_token})
+
+ self.launch(callback_function=callback)
+
+ # Need to wait for Spotify to be launched on Chromecast completely
+ while not self.is_launched:
+ time.sleep(1)
diff --git a/resources/pychromecast/pychromecast/controllers/youtube.py b/resources/pychromecast/pychromecast/controllers/youtube.py
new file mode 100644
index 0000000..fc8fd4f
--- /dev/null
+++ b/resources/pychromecast/pychromecast/controllers/youtube.py
@@ -0,0 +1,383 @@
+"""
+Controller to interface with the YouTube-app.
+"""
+
+import re
+import threading
+try:
+ from json import JSONDecodeError
+except ImportError:
+ JSONDecodeError = ValueError
+
+import requests
+from . import BaseController
+
+YOUTUBE_BASE_URL = "https://www.youtube.com/"
+YOUTUBE_WATCH_VIDEO_URL = YOUTUBE_BASE_URL + "watch?v="
+
+# id param is const(YouTube sets it as random xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx so it should be fine).
+RANDOM_ID = "12345678-9ABC-4DEF-0123-0123456789AB"
+VIDEO_ID_PARAM = '%7B%22videoId%22%3A%22{video_id}%22%2C%22currentTime%22%3A5%2C%22currentIndex%22%3A0%7D'
+TERMINATE_PARAM = "terminate"
+
+REQUEST_URL_SET_PLAYLIST = YOUTUBE_BASE_URL + "api/lounge/bc/bind?"
+BASE_REQUEST_PARAMS = {"device": "REMOTE_CONTROL", "id": RANDOM_ID, "name": "Desktop&app=youtube-desktop",
+ "mdx-version": 3, "loungeIdToken": None, "VER": 8, "v": 2, "t": 1, "ui": 1, "RID": 75956,
+ "CVER": 1}
+
+SET_PLAYLIST_METHOD = {"method": "setPlaylist", "params": VIDEO_ID_PARAM, "TYPE": None}
+REQUEST_PARAMS_SET_PLAYLIST = dict(**dict(BASE_REQUEST_PARAMS, **SET_PLAYLIST_METHOD))
+
+REQUEST_DATA_SET_PLAYLIST = "count=0"
+REQUEST_DATA_ADD_TO_PLAYLIST = "count=1&ofs=%d&req0__sc=addVideo&req0_videoId=%s"
+REQUEST_DATA_REMOVE_FROM_PLAYLIST = "count=1&ofs=%d&req0__sc=removeVideo&req0_videoId=%s"
+REQUEST_DATA_CLEAR_PLAYLIST = "count=1&ofs=%d&req0__sc=clearPlaylist"
+
+REQUEST_URL_LOUNGE_TOKEN = YOUTUBE_BASE_URL + "api/lounge/pairing/get_lounge_token_batch"
+REQUEST_DATA_LOUNGE_TOKEN = "screen_ids={screenId}&session_token={XSRFToken}"
+
+YOUTUBE_SESSION_TOKEN_REGEX = 'XSRF_TOKEN\W*(.*)="'
+SID_REGEX = '"c","(.*?)",\"'
+PLAYLIST_ID_REGEX = 'listId":"(.*?)"'
+FIRST_VIDEO_ID_REGEX = 'firstVideoId":"(.*?)"'
+GSESSION_ID_REGEX = '"S","(.*?)"]'
+NOW_PLAYING_REGEX = 'videoId":"(.*?)"'
+
+EXPIRED_LOUNGE_ID_RESPONSE_CONTENT = "Expired lounge id token"
+
+MEDIA_NAMESPACE = "urn:x-cast:com.google.cast.media"
+MESSAGE_TYPE = "type"
+TYPE_GET_SCREEN_ID = "getMdxSessionStatus"
+TYPE_STATUS = "mdxSessionStatus"
+ATTR_SCREEN_ID = "screenId"
+TYPE_PLAY = "PLAY"
+TYPE_PAUSE = "PAUSE"
+TYPE_STOP = "STOP"
+
+
+class YoutubeSessionError(Exception):
+ pass
+
+
+class YoutubeControllerError(Exception):
+ pass
+
+
+class YouTubeController(BaseController):
+ """ Controller to interact with Youtube."""
+
+ def __init__(self):
+ super(YouTubeController, self).__init__(
+ "urn:x-cast:com.google.youtube.mdx", "233637DE")
+
+ self._xsrf_token = None
+ self._lounge_token = None
+ self._gsession_id = None
+ self._sid = None
+ self._ofs = 0
+ self._first_video = None
+ self._playlist_id = None
+ self.screen_id = None
+ self.video_id = None
+ self.playlist = None
+ self._now_playing = None
+ self.status_update_event = threading.Event()
+
+ @property
+ def video_url(self):
+ """Returns the base watch video url with the current video_id"""
+ video = self._now_playing or self.video_id
+ return YOUTUBE_WATCH_VIDEO_URL + video
+
+ @property
+ def status(self):
+ """ Returns the media_controller status handler when Youtube app is launched."""
+ if self.is_active:
+ return self._socket_client.media_controller.status
+ else:
+ return None
+
+ @property
+ def in_session(self):
+ """ Returns True if session params are not None."""
+ if self._gsession_id and self._sid and self._lounge_token:
+ return True
+ else:
+ return False
+
+ def _do_post(self, url, data, params=None, referer=None):
+ """
+ Does all post requests.
+ will raise if response is not 200-ok
+ :param url:(str)the request url
+ :param data:(str) the request body
+ :param params:(dict) the request urlparams
+ :param referer:(str) the referer. default is the video url that started the session.
+ :return: the response
+ """
+ headers = {
+ "Origin": YOUTUBE_BASE_URL,
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Referer": (referer or self.video_url)
+ }
+ response = requests.post(url, headers=headers, data=data, params=params)
+ response.raise_for_status()
+ return response
+
+ def update_screen_id(self):
+ """
+ Sends a getMdxSessionStatus to get the screen id and waits for response.
+ This function is blocking but if connected we should always get a response
+ (send message will launch app if it is not running).
+ """
+ self.status_update_event.clear()
+ self.send_message({MESSAGE_TYPE: TYPE_GET_SCREEN_ID})
+ self.status_update_event.wait()
+ self.status_update_event.clear()
+
+ def _get_xsrf_token(self):
+ """
+ Get the xsrf_token used as the session token.
+ video_id must be initialized.
+ Sets the session token(xsrf_token).
+ """
+ if not self.video_id:
+ raise ValueError("Cant start a session without the video_id.")
+ response = requests.get(self.video_url)
+ response.raise_for_status()
+ token = re.search(YOUTUBE_SESSION_TOKEN_REGEX, str(response.content))
+ if not token:
+ raise YoutubeSessionError("Could not fetch the xsrf token")
+ self._xsrf_token = token.group(1)
+
+ def _get_lounge_id(self):
+ """
+ Gets the lounge_token.
+ session_token(xsrf_token) and screenId must be initialized.
+ Sets the lounge token.
+ """
+ if not self.screen_id:
+ raise ValueError("Screen id is None. update_screen_id must be called.")
+ if not self._xsrf_token:
+ raise ValueError("xsrf token is None. Get xsrf token must be called.")
+ data = REQUEST_DATA_LOUNGE_TOKEN.format(screenId=self.screen_id, XSRFToken=self._xsrf_token)
+ response = self._do_post(REQUEST_URL_LOUNGE_TOKEN, data=data)
+ if response.status_code == 401:
+ # Screen id is not None and it is updated with a message from the Chromecast.
+ # It is very unlikely that screen_id caused the problem.
+ raise YoutubeSessionError("Could not get lounge id. XSRF token has expired or is not valid.")
+ response.raise_for_status()
+ try:
+ lounge_token = response.json()["screens"][0]["loungeToken"]
+ except JSONDecodeError:
+ raise YoutubeSessionError("Could not get lounge id. XSRF token has expired or not valid.")
+ self._lounge_token = lounge_token
+
+ def _set_playlist(self):
+ """
+ Sends a POST to start the session.
+ Uses loung_token and video id as parameters.
+ Sets session SID and gsessionid on success.
+ """
+ if not self.video_id:
+ raise ValueError("Can't start a session without the video_id.")
+ if not self._lounge_token:
+ raise ValueError("lounge token is None. _get_lounge_token must be called")
+ url_params = REQUEST_PARAMS_SET_PLAYLIST.copy()
+ url_params['loungeIdToken'] = self._lounge_token
+ url_params['params'] = VIDEO_ID_PARAM.format(video_id=self.video_id)
+ response = self._do_post(REQUEST_URL_SET_PLAYLIST, data=REQUEST_DATA_SET_PLAYLIST, params=url_params)
+ content = str(response.content)
+ if response.status_code == 401 and content.find(EXPIRED_LOUNGE_ID_RESPONSE_CONTENT) != -1:
+ raise YoutubeSessionError("The lounge token expired.")
+ response.raise_for_status()
+ if not self.in_session:
+ self._extract_session_parameters(content)
+
+ def _update_session_parameters(self):
+ """
+ Sends a POST with no playlist parameters.
+ Gets the playlist id, SID, gsession id.
+ First video(the playlist base video) and now playing are also returned if playlist is initialized.
+ """
+ url_params = BASE_REQUEST_PARAMS.copy()
+ url_params['loungeIdToken'] = self._lounge_token
+ response = self._do_post(REQUEST_URL_SET_PLAYLIST, data='', params=url_params)
+ self._extract_session_parameters(str(response.content))
+ return response
+
+ def _extract_session_parameters(self, response_packet_content):
+ """
+ Extracts the playlist id, SID, gsession id, first video(the playlist base video)
+ and now playing from a session response.
+ :param response_packet_content: (str) the response packet content
+ """
+ content = response_packet_content
+ playlist_id = re.search(PLAYLIST_ID_REGEX, content)
+ sid = re.search(SID_REGEX, content)
+ gsession = re.search(GSESSION_ID_REGEX, content)
+ first_video = re.search(FIRST_VIDEO_ID_REGEX, content)
+ now_playing = re.search(NOW_PLAYING_REGEX, content)
+ if not (sid and gsession and playlist_id):
+ raise YoutubeSessionError("Could not parse session parameters.")
+ self._sid = sid.group(1)
+ self._gsession_id = gsession.group(1)
+ self._playlist_id = playlist_id.group(1)
+ if first_video:
+ self._first_video = first_video.group(1)
+ else:
+ self._first_video = None
+ if now_playing:
+ self._now_playing = now_playing.group(1)
+ else:
+ self._now_playing = None
+
+ def _manage_playlist(self, data, referer=None, **kwargs):
+ """
+ Manages all request to an existing session.
+ _gsession_id, _sid, video_id and _lounge_token must be initialized.
+ :param data: data of the request
+ :param video_id: video id in the request
+ :param refer: used for the request heders referer field.video_url by default.
+ """
+ if not self._gsession_id:
+ raise ValueError("gsession must be initialized to manage playlist")
+ if not self._sid:
+ raise ValueError("sid must be initialized to manage playlist")
+ if not self.video_id:
+ raise ValueError("video_id can't be empty")
+ if self.in_session:
+ self._update_session_parameters()
+ param_video_id = self._first_video or self.video_id
+
+ url_params = REQUEST_PARAMS_SET_PLAYLIST.copy()
+ url_params["loungeIdToken"] = self._lounge_token
+ url_params["params"] = VIDEO_ID_PARAM.format(video_id=param_video_id)
+ url_params["gsessionid"] = self._gsession_id
+ url_params["SID"] = self._sid
+ for key in kwargs:
+ if key in url_params:
+ url_params[key] = kwargs[key]
+ try:
+ self._do_post(REQUEST_URL_SET_PLAYLIST, referer=referer, data=data, params=url_params)
+ except requests.HTTPError:
+ # Try to re-get session variables and post again.
+ self._set_playlist()
+ url_params["loungeIdToken"] = self._lounge_token
+ url_params["params"] = VIDEO_ID_PARAM.format(video_id=self._first_video)
+ url_params["gsessionid"] = self._gsession_id
+ url_params["SID"] = self._sid
+ self._ofs = 0
+ self._do_post(REQUEST_URL_SET_PLAYLIST, referer=referer, data=data, params=url_params)
+
+ def clear_playlist(self, terminate_session=False):
+ """
+ clears all tracks on queue without closing the session.
+ terminate_session: close the existing session after clearing playlist.
+ App closes after a few minutes idle so terminate session if idle for a few minutes.
+ """
+ self._ofs += 1
+ self._manage_playlist(REQUEST_DATA_CLEAR_PLAYLIST % self._ofs)
+ if terminate_session:
+ self.terminate_session()
+ self.playlist = None
+
+ def terminate_session(self):
+ """
+ terminates the open lounge session.
+ """
+ try:
+ self.clear_playlist()
+ self._manage_playlist(data='', video_id=self.video_id, TYPE=TERMINATE_PARAM)
+ except requests.RequestException:
+ # Session has expired or not in sync.Clean session parameters anyway.
+ pass
+ self.screen_id = None
+ self.video_id = None
+ self._xsrf_token = None
+ self._lounge_token = None
+ self._gsession_id = None
+ self._sid = None
+ self._ofs = 0
+ self.playlist = None
+ self._first_video = None
+
+ def receive_message(self, message, data):
+ """ Called when a media message is received. """
+ if data[MESSAGE_TYPE] == TYPE_STATUS:
+ self._process_status(data.get("data"))
+
+ return True
+
+ else:
+ return False
+
+ def start_new_session(self, youtube_id):
+ self.video_id = youtube_id
+ self.update_screen_id()
+ self._get_xsrf_token()
+ self._get_lounge_id()
+ self._update_session_parameters()
+
+ def play_video(self, youtube_id):
+ """
+ Starts playing a video in the YouTube app.
+ The youtube id is also a session identifier used in all requests for the session.
+ :param youtube_id: The video id to play.
+ """
+ if not self.in_session:
+ self.start_new_session(youtube_id)
+ if self._first_video:
+ self.clear_playlist()
+ self._set_playlist()
+ self._update_session_parameters()
+
+ def add_to_queue(self, youtube_id):
+ """
+ Adds a video to the queue video will play after the currently playing video ends.
+ If video is buffering it wil not be added!
+ :param youtube_id: The video id to add to the queue
+ """
+ if not self.in_session:
+ raise YoutubeSessionError('Session must be initialized to add to queue')
+ if not self.playlist:
+ self.playlist = [self.video_id]
+ elif youtube_id in self.playlist:
+ raise YoutubeControllerError("Video already in queue")
+ self.update_screen_id()
+ # if self.status.player_is_idle:
+ # raise YoutubeControllerError("Can't add to queue while video is idle")
+ if self.status.player_state == "BUFFERING":
+ raise YoutubeControllerError("Can't add to queue while video is buffering")
+ self._ofs += 1
+ self._manage_playlist(data=REQUEST_DATA_ADD_TO_PLAYLIST % (self._ofs, youtube_id))
+ self.playlist.append(youtube_id)
+
+ def _send_command(self, message, namespace=MEDIA_NAMESPACE):
+ """
+ Sends a message to a specific namespace.
+ :param message:(dict) the message to sent to chromecast
+ :param namespace:(str) the namespace to send the message to. default is media namespace.
+ """
+ self._socket_client.send_app_message(namespace, message)
+
+ def play(self):
+ self._send_command({MESSAGE_TYPE: TYPE_PLAY})
+
+ def pause(self):
+ self._send_command({MESSAGE_TYPE: TYPE_PAUSE})
+
+ def stop(self, clear_queue=True):
+ if clear_queue:
+ self.clear_playlist()
+ self._send_command({MESSAGE_TYPE: TYPE_STOP})
+
+ def _process_status(self, status):
+ """ Process latest status update. """
+ self.screen_id = status.get(ATTR_SCREEN_ID)
+ self.status_update_event.set()
+
+ def tear_down(self):
+ """ Called when controller is destroyed. """
+ super(YouTubeController, self).tear_down()
+ self.terminate_session()
diff --git a/resources/pychromecast/pychromecast/controllers/youtube.py.bak b/resources/pychromecast/pychromecast/controllers/youtube.py.bak
new file mode 100644
index 0000000..b0f24cb
--- /dev/null
+++ b/resources/pychromecast/pychromecast/controllers/youtube.py.bak
@@ -0,0 +1,59 @@
+"""
+Controller to interface with the YouTube-app.
+
+Use the media controller to play, pause etc.
+"""
+from . import BaseController
+
+MESSAGE_TYPE = "type"
+TYPE_STATUS = "mdxSessionStatus"
+ATTR_SCREEN_ID = "screenId"
+
+
+class YouTubeController(BaseController):
+ """ Controller to interact with Youtube namespace. """
+
+ def __init__(self):
+ super(YouTubeController, self).__init__(
+ "urn:x-cast:com.google.youtube.mdx", "233637DE")
+
+ self.screen_id = None
+
+ def receive_message(self, message, data):
+ """ Called when a media message is received. """
+ if data[MESSAGE_TYPE] == TYPE_STATUS:
+ self._process_status(data.get('data'))
+
+ return True
+
+ return False
+
+ def play_video(self, youtube_id):
+ """
+ Starts playing a video in the YouTube app.
+
+ Only works if there is no video playing.
+ """
+ def callback():
+ """Plays requested video after app launched."""
+ self.start_play(youtube_id)
+
+ self.launch(callback_function=callback)
+
+ def start_play(self, youtube_id):
+ """
+ Sends the play message to the YouTube app.
+ """
+ msg = {
+ "type": "flingVideo",
+ "data": {
+ "currentTime": 0,
+ "videoId": youtube_id
+ }
+ }
+
+ self.send_message(msg, inc_session_id=True)
+
+ def _process_status(self, status):
+ """ Process latest status update. """
+ self.screen_id = status.get(ATTR_SCREEN_ID)
diff --git a/resources/pychromecast/pychromecast/dial.py b/resources/pychromecast/pychromecast/dial.py
new file mode 100644
index 0000000..d8456e5
--- /dev/null
+++ b/resources/pychromecast/pychromecast/dial.py
@@ -0,0 +1,89 @@
+"""
+Implements the DIAL-protocol to communicate with the Chromecast
+"""
+from collections import namedtuple
+from uuid import UUID
+
+import requests
+
+XML_NS_UPNP_DEVICE = "{urn:schemas-upnp-org:device-1-0}"
+
+FORMAT_BASE_URL = "http://{}:8008"
+
+CC_SESSION = requests.Session()
+CC_SESSION.headers['content-type'] = 'application/json'
+
+# Regular chromecast, supports video/audio
+CAST_TYPE_CHROMECAST = 'cast'
+# Cast Audio device, supports only audio
+CAST_TYPE_AUDIO = 'audio'
+# Cast Audio group device, supports only audio
+CAST_TYPE_GROUP = 'group'
+
+CAST_TYPES = {
+ 'chromecast': CAST_TYPE_CHROMECAST,
+ 'eureka dongle': CAST_TYPE_CHROMECAST,
+ 'chromecast audio': CAST_TYPE_AUDIO,
+ 'google home': CAST_TYPE_AUDIO,
+ 'google cast group': CAST_TYPE_GROUP,
+}
+
+
+def reboot(host):
+ """ Reboots the chromecast. """
+ CC_SESSION.post(FORMAT_BASE_URL.format(host) + "/setup/reboot",
+ data='{"params":"now"}', timeout=10)
+
+
+def get_device_status(host):
+ """
+ :param host: Hostname or ip to fetch status from
+ :type host: str
+ :return: The device status as a named tuple.
+ :rtype: pychromecast.dial.DeviceStatus or None
+ """
+
+ try:
+ req = CC_SESSION.get(
+ FORMAT_BASE_URL.format(host) + "/setup/eureka_info?options=detail",
+ timeout=10)
+
+ req.raise_for_status()
+
+ # The Requests library will fall back to guessing the encoding in case
+ # no encoding is specified in the response headers - which is the case
+ # for the Chromecast.
+ # The standard mandates utf-8 encoding, let's fall back to that instead
+ # if no encoding is provided, since the autodetection does not always
+ # provide correct results.
+ if req.encoding is None:
+ req.encoding = 'utf-8'
+
+ status = req.json()
+
+ friendly_name = status.get('name', "Unknown Chromecast")
+ model_name = "Unknown model name"
+ manufacturer = "Unknown manufacturer"
+ if 'detail' in status:
+ model_name = status['detail'].get('model_name', model_name)
+ manufacturer = status['detail'].get('manufacturer', manufacturer)
+
+ udn = status.get('ssdp_udn', None)
+
+ cast_type = CAST_TYPES.get(model_name.lower(),
+ CAST_TYPE_CHROMECAST)
+
+ uuid = None
+ if udn:
+ uuid = UUID(udn.replace('-', ''))
+
+ return DeviceStatus(friendly_name, model_name, manufacturer,
+ uuid, cast_type)
+
+ except (requests.exceptions.RequestException, ValueError):
+ return None
+
+
+DeviceStatus = namedtuple(
+ "DeviceStatus",
+ ["friendly_name", "model_name", "manufacturer", "uuid", "cast_type"])
diff --git a/resources/pychromecast/pychromecast/discovery.py b/resources/pychromecast/pychromecast/discovery.py
new file mode 100644
index 0000000..ed128e9
--- /dev/null
+++ b/resources/pychromecast/pychromecast/discovery.py
@@ -0,0 +1,130 @@
+"""Discovers Chromecasts on the network using mDNS/zeroconf."""
+import socket
+from uuid import UUID
+
+import zeroconf
+
+DISCOVER_TIMEOUT = 5
+
+
+class CastListener(object):
+ """Zeroconf Cast Services collection."""
+
+ def __init__(self, callback=None):
+ self.services = {}
+ self.callback = callback
+
+ @property
+ def count(self):
+ """Number of discovered cast services."""
+ return len(self.services)
+
+ @property
+ def devices(self):
+ """List of tuples (ip, host) for each discovered device."""
+ return list(self.services.values())
+
+ # pylint: disable=unused-argument
+ def remove_service(self, zconf, typ, name):
+ """ Remove a service from the collection. """
+ self.services.pop(name, None)
+
+ def add_service(self, zconf, typ, name):
+ """ Add a service to the collection. """
+ service = None
+ tries = 0
+ while service is None and tries < 4:
+ try:
+ service = zconf.get_service_info(typ, name)
+ except IOError:
+ # If the zerconf fails to receive the necesarry data we abort
+ # adding the service
+ break
+ tries += 1
+
+ if not service:
+ return
+
+ def get_value(key):
+ """Retrieve value and decode to UTF-8."""
+ value = service.properties.get(key.encode('utf-8'))
+
+ if value is None or isinstance(value, str):
+ return value
+ return value.decode('utf-8')
+
+ ips = zconf.cache.entries_with_name(service.server.lower())
+ host = repr(ips[0]) if ips else service.server
+
+ model_name = get_value('md')
+ uuid = get_value('id')
+ friendly_name = get_value('fn')
+
+ if uuid:
+ uuid = UUID(uuid)
+
+ self.services[name] = (host, service.port, uuid, model_name,
+ friendly_name)
+
+ if self.callback:
+ self.callback(name)
+
+
+def start_discovery(callback=None):
+ """
+ Start discovering chromecasts on the network.
+
+ This method will start discovering chromecasts on a separate thread. When
+ a chromecast is discovered, the callback will be called with the
+ discovered chromecast's zeroconf name. This is the dictionary key to find
+ the chromecast metadata in listener.services.
+
+ This method returns the CastListener object and the zeroconf ServiceBrowser
+ object. The CastListener object will contain information for the discovered
+ chromecasts. To stop discovery, call the stop_discovery method with the
+ ServiceBrowser object.
+ """
+ listener = CastListener(callback)
+ service_browser = False
+ try:
+ service_browser = zeroconf.ServiceBrowser(zeroconf.Zeroconf(),
+ "_googlecast._tcp.local.",
+ listener)
+ except (zeroconf.BadTypeInNameException,
+ NotImplementedError,
+ OSError,
+ socket.error,
+ zeroconf.NonUniqueNameException):
+ pass
+
+ return listener, service_browser
+
+
+def stop_discovery(browser):
+ """Stop the chromecast discovery thread."""
+ browser.zc.close()
+
+
+def discover_chromecasts(max_devices=None, timeout=DISCOVER_TIMEOUT):
+ """ Discover chromecasts on the network. """
+ from threading import Event
+ browser = False
+ try:
+ # pylint: disable=unused-argument
+ def callback(name):
+ """Called when zeroconf has discovered a new chromecast."""
+ if max_devices is not None and listener.count >= max_devices:
+ discover_complete.set()
+
+ discover_complete = Event()
+ listener, browser = start_discovery(callback)
+
+ # Wait for the timeout or the maximum number of devices
+ discover_complete.wait(timeout)
+
+ return listener.devices
+ except Exception: # pylint: disable=broad-except
+ raise
+ finally:
+ if browser is not False:
+ stop_discovery(browser)
diff --git a/resources/pychromecast/pychromecast/error.py b/resources/pychromecast/pychromecast/error.py
new file mode 100644
index 0000000..f214af7
--- /dev/null
+++ b/resources/pychromecast/pychromecast/error.py
@@ -0,0 +1,63 @@
+"""
+Errors to be used by PyChromecast.
+"""
+
+
+class PyChromecastError(Exception):
+ """ Base error for PyChromecast. """
+ pass
+
+
+class NoChromecastFoundError(PyChromecastError):
+ """
+ When a command has to auto-discover a Chromecast and cannot find one.
+ """
+ pass
+
+
+class MultipleChromecastsFoundError(PyChromecastError):
+ """
+ When getting a singular chromecast results in getting multiple chromecasts.
+ """
+ pass
+
+
+class ChromecastConnectionError(PyChromecastError):
+ """ When a connection error occurs within PyChromecast. """
+ pass
+
+
+class LaunchError(PyChromecastError):
+ """ When an app fails to launch. """
+ pass
+
+
+class PyChromecastStopped(PyChromecastError):
+ """ Raised when a command is invoked while the Chromecast's socket_client
+ is stopped.
+
+ """
+ pass
+
+
+class NotConnected(PyChromecastError):
+ """
+ Raised when a command is invoked while not connected to a Chromecast.
+ """
+ pass
+
+
+class UnsupportedNamespace(PyChromecastError):
+ """
+ Raised when trying to send a message with a namespace that is not
+ supported by the current running app.
+ """
+ pass
+
+
+class ControllerNotRegistered(PyChromecastError):
+ """
+ Raised when trying to interact with a controller while it is
+ not registered with a ChromeCast object.
+ """
+ pass
diff --git a/resources/pychromecast/pychromecast/logging_pb2.py b/resources/pychromecast/pychromecast/logging_pb2.py
new file mode 100644
index 0000000..8cb6816
--- /dev/null
+++ b/resources/pychromecast/pychromecast/logging_pb2.py
@@ -0,0 +1,869 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: logging.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='logging.proto',
+ package='extensions.api.cast_channel.proto',
+ syntax='proto2',
+ serialized_pb=_b('\n\rlogging.proto\x12!extensions.api.cast_channel.proto\"\xfd\x04\n\x0bSocketEvent\x12:\n\x04type\x18\x01 \x01(\x0e\x32,.extensions.api.cast_channel.proto.EventType\x12\x18\n\x10timestamp_micros\x18\x02 \x01(\x03\x12\x0f\n\x07\x64\x65tails\x18\x03 \x01(\t\x12\x18\n\x10net_return_value\x18\x04 \x01(\x05\x12\x19\n\x11message_namespace\x18\x05 \x01(\t\x12\x42\n\x0bready_state\x18\x06 \x01(\x0e\x32-.extensions.api.cast_channel.proto.ReadyState\x12L\n\x10\x63onnection_state\x18\x07 \x01(\x0e\x32\x32.extensions.api.cast_channel.proto.ConnectionState\x12@\n\nread_state\x18\x08 \x01(\x0e\x32,.extensions.api.cast_channel.proto.ReadState\x12\x42\n\x0bwrite_state\x18\t \x01(\x0e\x32-.extensions.api.cast_channel.proto.WriteState\x12\x42\n\x0b\x65rror_state\x18\n \x01(\x0e\x32-.extensions.api.cast_channel.proto.ErrorState\x12^\n\x1a\x63hallenge_reply_error_type\x18\x0b \x01(\x0e\x32:.extensions.api.cast_channel.proto.ChallengeReplyErrorType\x12\x16\n\x0enss_error_code\x18\x0c \x01(\x05\"\xf4\x01\n\x15\x41ggregatedSocketEvent\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x13\n\x0b\x65ndpoint_id\x18\x02 \x01(\x05\x12I\n\x11\x63hannel_auth_type\x18\x03 \x01(\x0e\x32..extensions.api.cast_channel.proto.ChannelAuth\x12\x44\n\x0csocket_event\x18\x04 \x03(\x0b\x32..extensions.api.cast_channel.proto.SocketEvent\x12\x12\n\nbytes_read\x18\x05 \x01(\x03\x12\x15\n\rbytes_written\x18\x06 \x01(\x03\"\xb1\x01\n\x03Log\x12Y\n\x17\x61ggregated_socket_event\x18\x01 \x03(\x0b\x32\x38.extensions.api.cast_channel.proto.AggregatedSocketEvent\x12,\n$num_evicted_aggregated_socket_events\x18\x02 \x01(\x05\x12!\n\x19num_evicted_socket_events\x18\x03 \x01(\x05*\xc0\x06\n\tEventType\x12\x16\n\x12\x45VENT_TYPE_UNKNOWN\x10\x00\x12\x17\n\x13\x43\x41ST_SOCKET_CREATED\x10\x01\x12\x17\n\x13READY_STATE_CHANGED\x10\x02\x12\x1c\n\x18\x43ONNECTION_STATE_CHANGED\x10\x03\x12\x16\n\x12READ_STATE_CHANGED\x10\x04\x12\x17\n\x13WRITE_STATE_CHANGED\x10\x05\x12\x17\n\x13\x45RROR_STATE_CHANGED\x10\x06\x12\x12\n\x0e\x43ONNECT_FAILED\x10\x07\x12\x16\n\x12TCP_SOCKET_CONNECT\x10\x08\x12\x1d\n\x19TCP_SOCKET_SET_KEEP_ALIVE\x10\t\x12\x18\n\x14SSL_CERT_WHITELISTED\x10\n\x12\x16\n\x12SSL_SOCKET_CONNECT\x10\x0b\x12\x15\n\x11SSL_INFO_OBTAINED\x10\x0c\x12\x1b\n\x17\x44\x45R_ENCODED_CERT_OBTAIN\x10\r\x12\x1c\n\x18RECEIVED_CHALLENGE_REPLY\x10\x0e\x12\x18\n\x14\x41UTH_CHALLENGE_REPLY\x10\x0f\x12\x15\n\x11\x43ONNECT_TIMED_OUT\x10\x10\x12\x17\n\x13SEND_MESSAGE_FAILED\x10\x11\x12\x14\n\x10MESSAGE_ENQUEUED\x10\x12\x12\x10\n\x0cSOCKET_WRITE\x10\x13\x12\x13\n\x0fMESSAGE_WRITTEN\x10\x14\x12\x0f\n\x0bSOCKET_READ\x10\x15\x12\x10\n\x0cMESSAGE_READ\x10\x16\x12\x11\n\rSOCKET_CLOSED\x10\x19\x12\x1f\n\x1bSSL_CERT_EXCESSIVE_LIFETIME\x10\x1a\x12\x1b\n\x17\x43HANNEL_POLICY_ENFORCED\x10\x1b\x12\x1f\n\x1bTCP_SOCKET_CONNECT_COMPLETE\x10\x1c\x12\x1f\n\x1bSSL_SOCKET_CONNECT_COMPLETE\x10\x1d\x12\x1d\n\x19SSL_SOCKET_CONNECT_FAILED\x10\x1e\x12\x1e\n\x1aSEND_AUTH_CHALLENGE_FAILED\x10\x1f\x12 \n\x1c\x41UTH_CHALLENGE_REPLY_INVALID\x10 \x12\x14\n\x10PING_WRITE_ERROR\x10!*(\n\x0b\x43hannelAuth\x12\x07\n\x03SSL\x10\x01\x12\x10\n\x0cSSL_VERIFIED\x10\x02*\x85\x01\n\nReadyState\x12\x14\n\x10READY_STATE_NONE\x10\x01\x12\x1a\n\x16READY_STATE_CONNECTING\x10\x02\x12\x14\n\x10READY_STATE_OPEN\x10\x03\x12\x17\n\x13READY_STATE_CLOSING\x10\x04\x12\x16\n\x12READY_STATE_CLOSED\x10\x05*\x8f\x03\n\x0f\x43onnectionState\x12\x16\n\x12\x43ONN_STATE_UNKNOWN\x10\x01\x12\x1a\n\x16\x43ONN_STATE_TCP_CONNECT\x10\x02\x12#\n\x1f\x43ONN_STATE_TCP_CONNECT_COMPLETE\x10\x03\x12\x1a\n\x16\x43ONN_STATE_SSL_CONNECT\x10\x04\x12#\n\x1f\x43ONN_STATE_SSL_CONNECT_COMPLETE\x10\x05\x12\"\n\x1e\x43ONN_STATE_AUTH_CHALLENGE_SEND\x10\x06\x12+\n\'CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE\x10\x07\x12,\n(CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE\x10\x08\x12\x1c\n\x18\x43ONN_STATE_START_CONNECT\x10\t\x12\x17\n\x13\x43ONN_STATE_FINISHED\x10\x64\x12\x14\n\x10\x43ONN_STATE_ERROR\x10\x65\x12\x16\n\x12\x43ONN_STATE_TIMEOUT\x10\x66*\xa5\x01\n\tReadState\x12\x16\n\x12READ_STATE_UNKNOWN\x10\x01\x12\x13\n\x0fREAD_STATE_READ\x10\x02\x12\x1c\n\x18READ_STATE_READ_COMPLETE\x10\x03\x12\x1a\n\x16READ_STATE_DO_CALLBACK\x10\x04\x12\x1b\n\x17READ_STATE_HANDLE_ERROR\x10\x05\x12\x14\n\x10READ_STATE_ERROR\x10\x64*\xc4\x01\n\nWriteState\x12\x17\n\x13WRITE_STATE_UNKNOWN\x10\x01\x12\x15\n\x11WRITE_STATE_WRITE\x10\x02\x12\x1e\n\x1aWRITE_STATE_WRITE_COMPLETE\x10\x03\x12\x1b\n\x17WRITE_STATE_DO_CALLBACK\x10\x04\x12\x1c\n\x18WRITE_STATE_HANDLE_ERROR\x10\x05\x12\x15\n\x11WRITE_STATE_ERROR\x10\x64\x12\x14\n\x10WRITE_STATE_IDLE\x10\x65*\xdb\x02\n\nErrorState\x12\x16\n\x12\x43HANNEL_ERROR_NONE\x10\x01\x12\"\n\x1e\x43HANNEL_ERROR_CHANNEL_NOT_OPEN\x10\x02\x12&\n\"CHANNEL_ERROR_AUTHENTICATION_ERROR\x10\x03\x12\x1f\n\x1b\x43HANNEL_ERROR_CONNECT_ERROR\x10\x04\x12\x1e\n\x1a\x43HANNEL_ERROR_SOCKET_ERROR\x10\x05\x12!\n\x1d\x43HANNEL_ERROR_TRANSPORT_ERROR\x10\x06\x12!\n\x1d\x43HANNEL_ERROR_INVALID_MESSAGE\x10\x07\x12$\n CHANNEL_ERROR_INVALID_CHANNEL_ID\x10\x08\x12!\n\x1d\x43HANNEL_ERROR_CONNECT_TIMEOUT\x10\t\x12\x19\n\x15\x43HANNEL_ERROR_UNKNOWN\x10\n*\xb0\x06\n\x17\x43hallengeReplyErrorType\x12\x1e\n\x1a\x43HALLENGE_REPLY_ERROR_NONE\x10\x01\x12)\n%CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY\x10\x02\x12,\n(CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE\x10\x03\x12$\n CHALLENGE_REPLY_ERROR_NO_PAYLOAD\x10\x04\x12\x30\n,CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED\x10\x05\x12\'\n#CHALLENGE_REPLY_ERROR_MESSAGE_ERROR\x10\x06\x12%\n!CHALLENGE_REPLY_ERROR_NO_RESPONSE\x10\x07\x12/\n+CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND\x10\x08\x12-\n)CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED\x10\t\x12\x37\n3CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA\x10\n\x12\x33\n/CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY\x10\x0b\x12/\n+CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH\x10\x0c\x12;\n7CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG\x10\r\x12=\n9CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE\x10\x0e\x12*\n&CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED\x10\x0f\x12%\n!CHALLENGE_REPLY_ERROR_CRL_INVALID\x10\x10\x12&\n\"CHALLENGE_REPLY_ERROR_CERT_REVOKED\x10\x11\x42\x02H\x03')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+_EVENTTYPE = _descriptor.EnumDescriptor(
+ name='EventType',
+ full_name='extensions.api.cast_channel.proto.EventType',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='EVENT_TYPE_UNKNOWN', index=0, number=0,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CAST_SOCKET_CREATED', index=1, number=1,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='READY_STATE_CHANGED', index=2, number=2,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONNECTION_STATE_CHANGED', index=3, number=3,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='READ_STATE_CHANGED', index=4, number=4,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='WRITE_STATE_CHANGED', index=5, number=5,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='ERROR_STATE_CHANGED', index=6, number=6,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONNECT_FAILED', index=7, number=7,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='TCP_SOCKET_CONNECT', index=8, number=8,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='TCP_SOCKET_SET_KEEP_ALIVE', index=9, number=9,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SSL_CERT_WHITELISTED', index=10, number=10,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SSL_SOCKET_CONNECT', index=11, number=11,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SSL_INFO_OBTAINED', index=12, number=12,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='DER_ENCODED_CERT_OBTAIN', index=13, number=13,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='RECEIVED_CHALLENGE_REPLY', index=14, number=14,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='AUTH_CHALLENGE_REPLY', index=15, number=15,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONNECT_TIMED_OUT', index=16, number=16,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SEND_MESSAGE_FAILED', index=17, number=17,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='MESSAGE_ENQUEUED', index=18, number=18,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SOCKET_WRITE', index=19, number=19,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='MESSAGE_WRITTEN', index=20, number=20,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SOCKET_READ', index=21, number=21,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='MESSAGE_READ', index=22, number=22,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SOCKET_CLOSED', index=23, number=25,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SSL_CERT_EXCESSIVE_LIFETIME', index=24, number=26,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHANNEL_POLICY_ENFORCED', index=25, number=27,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='TCP_SOCKET_CONNECT_COMPLETE', index=26, number=28,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SSL_SOCKET_CONNECT_COMPLETE', index=27, number=29,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SSL_SOCKET_CONNECT_FAILED', index=28, number=30,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SEND_AUTH_CHALLENGE_FAILED', index=29, number=31,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='AUTH_CHALLENGE_REPLY_INVALID', index=30, number=32,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='PING_WRITE_ERROR', index=31, number=33,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=1120,
+ serialized_end=1952,
+)
+_sym_db.RegisterEnumDescriptor(_EVENTTYPE)
+
+EventType = enum_type_wrapper.EnumTypeWrapper(_EVENTTYPE)
+_CHANNELAUTH = _descriptor.EnumDescriptor(
+ name='ChannelAuth',
+ full_name='extensions.api.cast_channel.proto.ChannelAuth',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='SSL', index=0, number=1,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='SSL_VERIFIED', index=1, number=2,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=1954,
+ serialized_end=1994,
+)
+_sym_db.RegisterEnumDescriptor(_CHANNELAUTH)
+
+ChannelAuth = enum_type_wrapper.EnumTypeWrapper(_CHANNELAUTH)
+_READYSTATE = _descriptor.EnumDescriptor(
+ name='ReadyState',
+ full_name='extensions.api.cast_channel.proto.ReadyState',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='READY_STATE_NONE', index=0, number=1,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='READY_STATE_CONNECTING', index=1, number=2,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='READY_STATE_OPEN', index=2, number=3,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='READY_STATE_CLOSING', index=3, number=4,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='READY_STATE_CLOSED', index=4, number=5,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=1997,
+ serialized_end=2130,
+)
+_sym_db.RegisterEnumDescriptor(_READYSTATE)
+
+ReadyState = enum_type_wrapper.EnumTypeWrapper(_READYSTATE)
+_CONNECTIONSTATE = _descriptor.EnumDescriptor(
+ name='ConnectionState',
+ full_name='extensions.api.cast_channel.proto.ConnectionState',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_UNKNOWN', index=0, number=1,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_TCP_CONNECT', index=1, number=2,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_TCP_CONNECT_COMPLETE', index=2, number=3,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_SSL_CONNECT', index=3, number=4,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_SSL_CONNECT_COMPLETE', index=4, number=5,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_AUTH_CHALLENGE_SEND', index=5, number=6,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE', index=6, number=7,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE', index=7, number=8,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_START_CONNECT', index=8, number=9,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_FINISHED', index=9, number=100,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_ERROR', index=10, number=101,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CONN_STATE_TIMEOUT', index=11, number=102,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=2133,
+ serialized_end=2532,
+)
+_sym_db.RegisterEnumDescriptor(_CONNECTIONSTATE)
+
+ConnectionState = enum_type_wrapper.EnumTypeWrapper(_CONNECTIONSTATE)
+_READSTATE = _descriptor.EnumDescriptor(
+ name='ReadState',
+ full_name='extensions.api.cast_channel.proto.ReadState',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='READ_STATE_UNKNOWN', index=0, number=1,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='READ_STATE_READ', index=1, number=2,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='READ_STATE_READ_COMPLETE', index=2, number=3,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='READ_STATE_DO_CALLBACK', index=3, number=4,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='READ_STATE_HANDLE_ERROR', index=4, number=5,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='READ_STATE_ERROR', index=5, number=100,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=2535,
+ serialized_end=2700,
+)
+_sym_db.RegisterEnumDescriptor(_READSTATE)
+
+ReadState = enum_type_wrapper.EnumTypeWrapper(_READSTATE)
+_WRITESTATE = _descriptor.EnumDescriptor(
+ name='WriteState',
+ full_name='extensions.api.cast_channel.proto.WriteState',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='WRITE_STATE_UNKNOWN', index=0, number=1,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='WRITE_STATE_WRITE', index=1, number=2,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='WRITE_STATE_WRITE_COMPLETE', index=2, number=3,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='WRITE_STATE_DO_CALLBACK', index=3, number=4,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='WRITE_STATE_HANDLE_ERROR', index=4, number=5,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='WRITE_STATE_ERROR', index=5, number=100,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='WRITE_STATE_IDLE', index=6, number=101,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=2703,
+ serialized_end=2899,
+)
+_sym_db.RegisterEnumDescriptor(_WRITESTATE)
+
+WriteState = enum_type_wrapper.EnumTypeWrapper(_WRITESTATE)
+_ERRORSTATE = _descriptor.EnumDescriptor(
+ name='ErrorState',
+ full_name='extensions.api.cast_channel.proto.ErrorState',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='CHANNEL_ERROR_NONE', index=0, number=1,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHANNEL_ERROR_CHANNEL_NOT_OPEN', index=1, number=2,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHANNEL_ERROR_AUTHENTICATION_ERROR', index=2, number=3,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHANNEL_ERROR_CONNECT_ERROR', index=3, number=4,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHANNEL_ERROR_SOCKET_ERROR', index=4, number=5,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHANNEL_ERROR_TRANSPORT_ERROR', index=5, number=6,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHANNEL_ERROR_INVALID_MESSAGE', index=6, number=7,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHANNEL_ERROR_INVALID_CHANNEL_ID', index=7, number=8,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHANNEL_ERROR_CONNECT_TIMEOUT', index=8, number=9,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHANNEL_ERROR_UNKNOWN', index=9, number=10,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=2902,
+ serialized_end=3249,
+)
+_sym_db.RegisterEnumDescriptor(_ERRORSTATE)
+
+ErrorState = enum_type_wrapper.EnumTypeWrapper(_ERRORSTATE)
+_CHALLENGEREPLYERRORTYPE = _descriptor.EnumDescriptor(
+ name='ChallengeReplyErrorType',
+ full_name='extensions.api.cast_channel.proto.ChallengeReplyErrorType',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_NONE', index=0, number=1,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY', index=1, number=2,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE', index=2, number=3,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_NO_PAYLOAD', index=3, number=4,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED', index=4, number=5,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_MESSAGE_ERROR', index=5, number=6,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_NO_RESPONSE', index=6, number=7,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND', index=7, number=8,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED', index=8, number=9,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA', index=9, number=10,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY', index=10, number=11,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH', index=11, number=12,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG', index=12, number=13,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE', index=13, number=14,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED', index=14, number=15,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_CRL_INVALID', index=15, number=16,
+ options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='CHALLENGE_REPLY_ERROR_CERT_REVOKED', index=16, number=17,
+ options=None,
+ type=None),
+ ],
+ containing_type=None,
+ options=None,
+ serialized_start=3252,
+ serialized_end=4068,
+)
+_sym_db.RegisterEnumDescriptor(_CHALLENGEREPLYERRORTYPE)
+
+ChallengeReplyErrorType = enum_type_wrapper.EnumTypeWrapper(_CHALLENGEREPLYERRORTYPE)
+EVENT_TYPE_UNKNOWN = 0
+CAST_SOCKET_CREATED = 1
+READY_STATE_CHANGED = 2
+CONNECTION_STATE_CHANGED = 3
+READ_STATE_CHANGED = 4
+WRITE_STATE_CHANGED = 5
+ERROR_STATE_CHANGED = 6
+CONNECT_FAILED = 7
+TCP_SOCKET_CONNECT = 8
+TCP_SOCKET_SET_KEEP_ALIVE = 9
+SSL_CERT_WHITELISTED = 10
+SSL_SOCKET_CONNECT = 11
+SSL_INFO_OBTAINED = 12
+DER_ENCODED_CERT_OBTAIN = 13
+RECEIVED_CHALLENGE_REPLY = 14
+AUTH_CHALLENGE_REPLY = 15
+CONNECT_TIMED_OUT = 16
+SEND_MESSAGE_FAILED = 17
+MESSAGE_ENQUEUED = 18
+SOCKET_WRITE = 19
+MESSAGE_WRITTEN = 20
+SOCKET_READ = 21
+MESSAGE_READ = 22
+SOCKET_CLOSED = 25
+SSL_CERT_EXCESSIVE_LIFETIME = 26
+CHANNEL_POLICY_ENFORCED = 27
+TCP_SOCKET_CONNECT_COMPLETE = 28
+SSL_SOCKET_CONNECT_COMPLETE = 29
+SSL_SOCKET_CONNECT_FAILED = 30
+SEND_AUTH_CHALLENGE_FAILED = 31
+AUTH_CHALLENGE_REPLY_INVALID = 32
+PING_WRITE_ERROR = 33
+SSL = 1
+SSL_VERIFIED = 2
+READY_STATE_NONE = 1
+READY_STATE_CONNECTING = 2
+READY_STATE_OPEN = 3
+READY_STATE_CLOSING = 4
+READY_STATE_CLOSED = 5
+CONN_STATE_UNKNOWN = 1
+CONN_STATE_TCP_CONNECT = 2
+CONN_STATE_TCP_CONNECT_COMPLETE = 3
+CONN_STATE_SSL_CONNECT = 4
+CONN_STATE_SSL_CONNECT_COMPLETE = 5
+CONN_STATE_AUTH_CHALLENGE_SEND = 6
+CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE = 7
+CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE = 8
+CONN_STATE_START_CONNECT = 9
+CONN_STATE_FINISHED = 100
+CONN_STATE_ERROR = 101
+CONN_STATE_TIMEOUT = 102
+READ_STATE_UNKNOWN = 1
+READ_STATE_READ = 2
+READ_STATE_READ_COMPLETE = 3
+READ_STATE_DO_CALLBACK = 4
+READ_STATE_HANDLE_ERROR = 5
+READ_STATE_ERROR = 100
+WRITE_STATE_UNKNOWN = 1
+WRITE_STATE_WRITE = 2
+WRITE_STATE_WRITE_COMPLETE = 3
+WRITE_STATE_DO_CALLBACK = 4
+WRITE_STATE_HANDLE_ERROR = 5
+WRITE_STATE_ERROR = 100
+WRITE_STATE_IDLE = 101
+CHANNEL_ERROR_NONE = 1
+CHANNEL_ERROR_CHANNEL_NOT_OPEN = 2
+CHANNEL_ERROR_AUTHENTICATION_ERROR = 3
+CHANNEL_ERROR_CONNECT_ERROR = 4
+CHANNEL_ERROR_SOCKET_ERROR = 5
+CHANNEL_ERROR_TRANSPORT_ERROR = 6
+CHANNEL_ERROR_INVALID_MESSAGE = 7
+CHANNEL_ERROR_INVALID_CHANNEL_ID = 8
+CHANNEL_ERROR_CONNECT_TIMEOUT = 9
+CHANNEL_ERROR_UNKNOWN = 10
+CHALLENGE_REPLY_ERROR_NONE = 1
+CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY = 2
+CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE = 3
+CHALLENGE_REPLY_ERROR_NO_PAYLOAD = 4
+CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED = 5
+CHALLENGE_REPLY_ERROR_MESSAGE_ERROR = 6
+CHALLENGE_REPLY_ERROR_NO_RESPONSE = 7
+CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND = 8
+CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED = 9
+CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA = 10
+CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY = 11
+CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH = 12
+CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG = 13
+CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE = 14
+CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED = 15
+CHALLENGE_REPLY_ERROR_CRL_INVALID = 16
+CHALLENGE_REPLY_ERROR_CERT_REVOKED = 17
+
+
+
+_SOCKETEVENT = _descriptor.Descriptor(
+ name='SocketEvent',
+ full_name='extensions.api.cast_channel.proto.SocketEvent',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='type', full_name='extensions.api.cast_channel.proto.SocketEvent.type', index=0,
+ number=1, type=14, cpp_type=8, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='timestamp_micros', full_name='extensions.api.cast_channel.proto.SocketEvent.timestamp_micros', index=1,
+ number=2, type=3, cpp_type=2, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='details', full_name='extensions.api.cast_channel.proto.SocketEvent.details', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='net_return_value', full_name='extensions.api.cast_channel.proto.SocketEvent.net_return_value', index=3,
+ number=4, type=5, cpp_type=1, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='message_namespace', full_name='extensions.api.cast_channel.proto.SocketEvent.message_namespace', index=4,
+ number=5, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='ready_state', full_name='extensions.api.cast_channel.proto.SocketEvent.ready_state', index=5,
+ number=6, type=14, cpp_type=8, label=1,
+ has_default_value=False, default_value=1,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='connection_state', full_name='extensions.api.cast_channel.proto.SocketEvent.connection_state', index=6,
+ number=7, type=14, cpp_type=8, label=1,
+ has_default_value=False, default_value=1,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='read_state', full_name='extensions.api.cast_channel.proto.SocketEvent.read_state', index=7,
+ number=8, type=14, cpp_type=8, label=1,
+ has_default_value=False, default_value=1,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='write_state', full_name='extensions.api.cast_channel.proto.SocketEvent.write_state', index=8,
+ number=9, type=14, cpp_type=8, label=1,
+ has_default_value=False, default_value=1,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='error_state', full_name='extensions.api.cast_channel.proto.SocketEvent.error_state', index=9,
+ number=10, type=14, cpp_type=8, label=1,
+ has_default_value=False, default_value=1,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='challenge_reply_error_type', full_name='extensions.api.cast_channel.proto.SocketEvent.challenge_reply_error_type', index=10,
+ number=11, type=14, cpp_type=8, label=1,
+ has_default_value=False, default_value=1,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='nss_error_code', full_name='extensions.api.cast_channel.proto.SocketEvent.nss_error_code', index=11,
+ number=12, type=5, cpp_type=1, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=53,
+ serialized_end=690,
+)
+
+
+_AGGREGATEDSOCKETEVENT = _descriptor.Descriptor(
+ name='AggregatedSocketEvent',
+ full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='id', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.id', index=0,
+ number=1, type=5, cpp_type=1, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='endpoint_id', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.endpoint_id', index=1,
+ number=2, type=5, cpp_type=1, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='channel_auth_type', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.channel_auth_type', index=2,
+ number=3, type=14, cpp_type=8, label=1,
+ has_default_value=False, default_value=1,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='socket_event', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.socket_event', index=3,
+ number=4, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='bytes_read', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.bytes_read', index=4,
+ number=5, type=3, cpp_type=2, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='bytes_written', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.bytes_written', index=5,
+ number=6, type=3, cpp_type=2, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=693,
+ serialized_end=937,
+)
+
+
+_LOG = _descriptor.Descriptor(
+ name='Log',
+ full_name='extensions.api.cast_channel.proto.Log',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='aggregated_socket_event', full_name='extensions.api.cast_channel.proto.Log.aggregated_socket_event', index=0,
+ number=1, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='num_evicted_aggregated_socket_events', full_name='extensions.api.cast_channel.proto.Log.num_evicted_aggregated_socket_events', index=1,
+ number=2, type=5, cpp_type=1, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='num_evicted_socket_events', full_name='extensions.api.cast_channel.proto.Log.num_evicted_socket_events', index=2,
+ number=3, type=5, cpp_type=1, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=940,
+ serialized_end=1117,
+)
+
+_SOCKETEVENT.fields_by_name['type'].enum_type = _EVENTTYPE
+_SOCKETEVENT.fields_by_name['ready_state'].enum_type = _READYSTATE
+_SOCKETEVENT.fields_by_name['connection_state'].enum_type = _CONNECTIONSTATE
+_SOCKETEVENT.fields_by_name['read_state'].enum_type = _READSTATE
+_SOCKETEVENT.fields_by_name['write_state'].enum_type = _WRITESTATE
+_SOCKETEVENT.fields_by_name['error_state'].enum_type = _ERRORSTATE
+_SOCKETEVENT.fields_by_name['challenge_reply_error_type'].enum_type = _CHALLENGEREPLYERRORTYPE
+_AGGREGATEDSOCKETEVENT.fields_by_name['channel_auth_type'].enum_type = _CHANNELAUTH
+_AGGREGATEDSOCKETEVENT.fields_by_name['socket_event'].message_type = _SOCKETEVENT
+_LOG.fields_by_name['aggregated_socket_event'].message_type = _AGGREGATEDSOCKETEVENT
+DESCRIPTOR.message_types_by_name['SocketEvent'] = _SOCKETEVENT
+DESCRIPTOR.message_types_by_name['AggregatedSocketEvent'] = _AGGREGATEDSOCKETEVENT
+DESCRIPTOR.message_types_by_name['Log'] = _LOG
+DESCRIPTOR.enum_types_by_name['EventType'] = _EVENTTYPE
+DESCRIPTOR.enum_types_by_name['ChannelAuth'] = _CHANNELAUTH
+DESCRIPTOR.enum_types_by_name['ReadyState'] = _READYSTATE
+DESCRIPTOR.enum_types_by_name['ConnectionState'] = _CONNECTIONSTATE
+DESCRIPTOR.enum_types_by_name['ReadState'] = _READSTATE
+DESCRIPTOR.enum_types_by_name['WriteState'] = _WRITESTATE
+DESCRIPTOR.enum_types_by_name['ErrorState'] = _ERRORSTATE
+DESCRIPTOR.enum_types_by_name['ChallengeReplyErrorType'] = _CHALLENGEREPLYERRORTYPE
+
+SocketEvent = _reflection.GeneratedProtocolMessageType('SocketEvent', (_message.Message,), dict(
+ DESCRIPTOR = _SOCKETEVENT,
+ __module__ = 'logging_pb2'
+ # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.SocketEvent)
+ ))
+_sym_db.RegisterMessage(SocketEvent)
+
+AggregatedSocketEvent = _reflection.GeneratedProtocolMessageType('AggregatedSocketEvent', (_message.Message,), dict(
+ DESCRIPTOR = _AGGREGATEDSOCKETEVENT,
+ __module__ = 'logging_pb2'
+ # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.AggregatedSocketEvent)
+ ))
+_sym_db.RegisterMessage(AggregatedSocketEvent)
+
+Log = _reflection.GeneratedProtocolMessageType('Log', (_message.Message,), dict(
+ DESCRIPTOR = _LOG,
+ __module__ = 'logging_pb2'
+ # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.Log)
+ ))
+_sym_db.RegisterMessage(Log)
+
+
+DESCRIPTOR.has_options = True
+DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003'))
+# @@protoc_insertion_point(module_scope)
diff --git a/resources/pychromecast/pychromecast/socket_client.py b/resources/pychromecast/pychromecast/socket_client.py
new file mode 100644
index 0000000..cd32011
--- /dev/null
+++ b/resources/pychromecast/pychromecast/socket_client.py
@@ -0,0 +1,997 @@
+"""
+Module to interact with the ChromeCast via protobuf-over-socket.
+
+Big thanks goes out to Fred Clift who build the first
+version of this code: https://github.com/minektur/chromecast-python-poc.
+Without him this would not have been possible.
+"""
+# Pylint does not understand the protobuf objects correctly
+# pylint: disable=no-member
+
+import errno
+import json
+import logging
+import select
+import socket
+import ssl
+import sys
+import threading
+import time
+from collections import namedtuple
+from struct import pack, unpack
+
+from . import cast_channel_pb2
+from .controllers import BaseController
+from .controllers.media import MediaController
+from .dial import CAST_TYPE_CHROMECAST, CAST_TYPE_AUDIO, CAST_TYPE_GROUP
+from .error import (
+ ChromecastConnectionError,
+ UnsupportedNamespace,
+ NotConnected,
+ PyChromecastStopped,
+ LaunchError,
+)
+
+NS_CONNECTION = 'urn:x-cast:com.google.cast.tp.connection'
+NS_RECEIVER = 'urn:x-cast:com.google.cast.receiver'
+NS_HEARTBEAT = 'urn:x-cast:com.google.cast.tp.heartbeat'
+
+PLATFORM_DESTINATION_ID = "receiver-0"
+
+MESSAGE_TYPE = 'type'
+TYPE_PING = "PING"
+TYPE_RECEIVER_STATUS = "RECEIVER_STATUS"
+TYPE_PONG = "PONG"
+TYPE_CONNECT = "CONNECT"
+TYPE_CLOSE = "CLOSE"
+TYPE_GET_STATUS = "GET_STATUS"
+TYPE_LAUNCH = "LAUNCH"
+TYPE_LAUNCH_ERROR = "LAUNCH_ERROR"
+TYPE_LOAD = "LOAD"
+
+# The socket connection is being setup
+CONNECTION_STATUS_CONNECTING = "CONNECTING"
+# The socket connection was complete
+CONNECTION_STATUS_CONNECTED = "CONNECTED"
+# The socket connection has been disconnected
+CONNECTION_STATUS_DISCONNECTED = "DISCONNECTED"
+# Connecting to socket failed (after a CONNECTION_STATUS_CONNECTING)
+CONNECTION_STATUS_FAILED = "FAILED"
+# The socket connection was lost and needs to be retried
+CONNECTION_STATUS_LOST = "LOST"
+
+APP_ID = 'appId'
+REQUEST_ID = "requestId"
+SESSION_ID = "sessionId"
+ERROR_REASON = 'reason'
+
+HB_PING_TIME = 10
+HB_PONG_TIME = 10
+POLL_TIME_BLOCKING = 5.0
+POLL_TIME_NON_BLOCKING = 0.01
+TIMEOUT_TIME = 30
+RETRY_TIME = 5
+
+
+class InterruptLoop(Exception):
+ """ The chromecast has been manually stopped. """
+ pass
+
+
+def _json_from_message(message):
+ """ Parses a PB2 message into JSON format. """
+ try:
+ return json.loads(message.payload_utf8)
+ except ValueError:
+ logger = logging.getLogger(__name__)
+ logger.warning("Ignoring invalid json in namespace %s: %s",
+ message.namespace, message.payload_utf8)
+ return {}
+
+
+def _message_to_string(message, data=None):
+ """ Gives a string representation of a PB2 message. """
+ if data is None:
+ data = _json_from_message(message)
+
+ return "Message {} from {} to {}: {}".format(
+ message.namespace, message.source_id, message.destination_id, data)
+
+
+if sys.version_info >= (3, 0):
+ def _json_to_payload(data):
+ """ Encodes a python value into JSON format. """
+ return json.dumps(data, ensure_ascii=False).encode("utf8")
+else:
+ def _json_to_payload(data):
+ """ Encodes a python value into JSON format. """
+ return json.dumps(data, ensure_ascii=False)
+
+
+def _is_ssl_timeout(exc):
+ """ Returns True if the exception is for an SSL timeout """
+ return exc.message in ("The handshake operation timed out",
+ "The write operation timed out",
+ "The read operation timed out")
+
+
+NetworkAddress = namedtuple('NetworkAddress',
+ ['address', 'port'])
+
+ConnectionStatus = namedtuple('ConnectionStatus',
+ ['status', 'address'])
+
+CastStatus = namedtuple('CastStatus',
+ ['is_active_input', 'is_stand_by', 'volume_level',
+ 'volume_muted', 'app_id', 'display_name',
+ 'namespaces', 'session_id', 'transport_id',
+ 'status_text'])
+
+LaunchFailure = namedtuple('LaunchStatus',
+ ['reason', 'app_id', 'request_id'])
+
+
+# pylint: disable=too-many-instance-attributes
+class SocketClient(threading.Thread):
+ """
+ Class to interact with a Chromecast through a socket.
+
+ :param port: The port to use when connecting to the device, set to None to
+ use the default of 8009. Special devices such as Cast Groups
+ may return a different port number so we need to use that.
+ :param cast_type: The type of chromecast to connect to, see
+ dial.CAST_TYPE_* for types.
+ :param tries: Number of retries to perform if the connection fails.
+ None for inifinite retries.
+ :param retry_wait: A floating point number specifying how many seconds to
+ wait between each retry. None means to use the default
+ which is 5 seconds.
+ """
+
+ def __init__(self, host, port=None, cast_type=CAST_TYPE_CHROMECAST,
+ **kwargs):
+ tries = kwargs.pop('tries', None)
+ timeout = kwargs.pop('timeout', None)
+ retry_wait = kwargs.pop('retry_wait', None)
+ self.blocking = kwargs.pop('blocking', True)
+
+ if self.blocking:
+ self.polltime = POLL_TIME_BLOCKING
+ else:
+ self.polltime = POLL_TIME_NON_BLOCKING
+
+ super(SocketClient, self).__init__()
+
+ self.daemon = True
+
+ self.logger = logging.getLogger(__name__)
+
+ self._force_recon = False
+
+ self.cast_type = cast_type
+ self.tries = tries
+ self.timeout = timeout or TIMEOUT_TIME
+ self.retry_wait = retry_wait or RETRY_TIME
+ self.host = host
+ self.port = port or 8009
+
+ self.source_id = "sender-0"
+ self.stop = threading.Event()
+
+ self.app_namespaces = []
+ self.destination_id = None
+ self.session_id = None
+ self._request_id = 0
+ # dict mapping requestId on threading.Event objects
+ self._request_callbacks = {}
+ self._open_channels = []
+
+ self.connecting = True
+ self.socket = None
+
+ # dict mapping namespace on Controller objects
+ self._handlers = {}
+ self._connection_listeners = []
+
+ self.receiver_controller = ReceiverController(cast_type, self.blocking)
+ self.media_controller = MediaController()
+ self.heartbeat_controller = HeartbeatController()
+
+ self.register_handler(self.heartbeat_controller)
+ self.register_handler(ConnectionController())
+ self.register_handler(self.receiver_controller)
+ self.register_handler(self.media_controller)
+
+ self.receiver_controller.register_status_listener(self)
+
+ try:
+ self.initialize_connection()
+ except ChromecastConnectionError:
+ self._report_connection_status(
+ ConnectionStatus(CONNECTION_STATUS_DISCONNECTED,
+ NetworkAddress(self.host, self.port)))
+ raise
+
+ def initialize_connection(self):
+ """Initialize a socket to a Chromecast, retrying as necessary."""
+ tries = self.tries
+
+ if self.socket is not None:
+ self.socket.close()
+ self.socket = None
+
+ # Make sure nobody is blocking.
+ for callback in self._request_callbacks.values():
+ callback['event'].set()
+
+ self.app_namespaces = []
+ self.destination_id = None
+ self.session_id = None
+ self._request_id = 0
+ self._request_callbacks = {}
+ self._open_channels = []
+
+ self.connecting = True
+ retry_log_fun = self.logger.error
+
+ while not self.stop.is_set() and (tries is None or tries > 0):
+ try:
+ self.socket = new_socket()
+ self.socket.settimeout(self.timeout)
+ self._report_connection_status(
+ ConnectionStatus(CONNECTION_STATUS_CONNECTING,
+ NetworkAddress(self.host, self.port)))
+ self.socket.connect((self.host, self.port))
+ self.socket = ssl.wrap_socket(self.socket)
+ self.connecting = False
+ self._force_recon = False
+ self._report_connection_status(
+ ConnectionStatus(CONNECTION_STATUS_CONNECTED,
+ NetworkAddress(self.host, self.port)))
+ self.receiver_controller.update_status()
+ self.heartbeat_controller.ping()
+ self.heartbeat_controller.reset()
+
+ self.logger.debug("Connected!")
+ break
+ except OSError as err:
+ self.connecting = True
+ if self.stop.is_set():
+ self.logger.error(
+ "Failed to connect: %s. aborting due to stop signal.",
+ err)
+ raise ChromecastConnectionError("Failed to connect")
+
+ self._report_connection_status(
+ ConnectionStatus(CONNECTION_STATUS_FAILED,
+ NetworkAddress(self.host, self.port)))
+
+ # Only sleep if we have another retry remaining
+ if tries is None or tries > 1:
+ retry_log_fun("Failed to connect, retrying in %.1fs",
+ self.retry_wait)
+ retry_log_fun = self.logger.debug
+ time.sleep(self.retry_wait)
+
+ if tries:
+ tries -= 1
+ else:
+ self.stop.set()
+ self.logger.error("Failed to connect. No retries.")
+ raise ChromecastConnectionError("Failed to connect")
+
+ def disconnect(self):
+ """ Disconnect socket connection to Chromecast device """
+ self.stop.set()
+
+ def register_handler(self, handler):
+ """ Register a new namespace handler. """
+ self._handlers[handler.namespace] = handler
+
+ handler.registered(self)
+
+ def new_cast_status(self, cast_status):
+ """ Called when a new cast status has been received. """
+ new_channel = self.destination_id != cast_status.transport_id
+
+ if new_channel:
+ self.disconnect_channel(self.destination_id)
+
+ self.app_namespaces = cast_status.namespaces
+ self.destination_id = cast_status.transport_id
+ self.session_id = cast_status.session_id
+
+ if new_channel:
+ # If any of the namespaces of the new app are supported
+ # we will automatically connect to it to receive updates
+ for namespace in self.app_namespaces:
+ if namespace in self._handlers:
+ self._ensure_channel_connected(self.destination_id)
+ self._handlers[namespace].channel_connected()
+
+ def _gen_request_id(self):
+ """ Generates a unique request id. """
+ self._request_id += 1
+
+ return self._request_id
+
+ @property
+ def is_connected(self):
+ """
+ Returns True if the client is connected, False if it is stopped
+ (or trying to connect).
+ """
+ return not self.connecting
+
+ @property
+ def is_stopped(self):
+ """
+ Returns True if the connection has been stopped, False if it is
+ running.
+ """
+ return self.stop.is_set()
+
+ def run(self):
+ """ Start polling the socket. """
+ self.heartbeat_controller.reset()
+ self._force_recon = False
+ logging.debug("Thread started...")
+ while not self.stop.is_set():
+
+ if self.run_once() == 1:
+ break
+
+ # Clean up
+ self._cleanup()
+
+ def run_once(self):
+ """
+ Use run_once() in your own main loop after you
+ receive something on the socket (get_socket()).
+ """
+ # pylint: disable=too-many-branches, too-many-return-statements
+
+ try:
+ if not self._check_connection():
+ return 0
+ except ChromecastConnectionError:
+ return 1
+
+ # poll the socket
+ can_read, _, _ = select.select([self.socket], [], [], self.polltime)
+
+ # read messages from chromecast
+ message = data = None
+ if self.socket in can_read and not self._force_recon:
+ try:
+ message = self._read_message()
+ except InterruptLoop as exc:
+ if self.stop.is_set():
+ self.logger.info(
+ "Stopped while reading message, disconnecting.")
+ else:
+ self.logger.error(
+ "Interruption caught without being stopped: %s",
+ exc)
+ return 1
+ except ssl.SSLError as exc:
+ if exc.errno == ssl.SSL_ERROR_EOF:
+ if self.stop.is_set():
+ return 1
+ raise
+ except socket.error:
+ self._force_recon = True
+ self.logger.error('Error reading from socket.')
+ else:
+ data = _json_from_message(message)
+ if not message:
+ return 0
+
+ # If we are stopped after receiving a message we skip the message
+ # and tear down the connection
+ if self.stop.is_set():
+ return 1
+
+ # See if any handlers will accept this message
+ self._route_message(message, data)
+
+ if REQUEST_ID in data:
+ callback = self._request_callbacks.pop(data[REQUEST_ID], None)
+ if callback is not None:
+ event = callback['event']
+ callback['response'] = data
+ function = callback['function']
+ event.set()
+ if function:
+ function(data)
+
+ return 0
+
+ def get_socket(self):
+ """
+ Returns the socket of the connection to use it in you own
+ main loop.
+ """
+ return self.socket
+
+ def _check_connection(self):
+ """
+ Checks if the connection is active, and if not reconnect
+
+ :return: True if the connection is active, False if the connection was
+ reset.
+ """
+ # check if connection is expired
+ reset = False
+ if self._force_recon:
+ self.logger.warning(
+ "Error communicating with socket, resetting connection")
+ reset = True
+
+ elif self.heartbeat_controller.is_expired():
+ self.logger.warning("Heartbeat timeout, resetting connection")
+ reset = True
+
+ if reset:
+ self._report_connection_status(
+ ConnectionStatus(CONNECTION_STATUS_LOST,
+ NetworkAddress(self.host, self.port)))
+ try:
+ self.initialize_connection()
+ except ChromecastConnectionError:
+ self.stop.set()
+ return False
+ return True
+
+ def _route_message(self, message, data):
+ """ Route message to any handlers on the message namespace """
+ # route message to handlers
+ if message.namespace in self._handlers:
+
+ # debug messages
+ if message.namespace != NS_HEARTBEAT:
+ self.logger.debug(
+ "Received: %s", _message_to_string(message, data))
+
+ # message handlers
+ try:
+ handled = \
+ self._handlers[message.namespace].receive_message(
+ message, data)
+
+ if not handled:
+ if data.get(REQUEST_ID) not in self._request_callbacks:
+ self.logger.debug(
+ "Message unhandled: %s",
+ _message_to_string(message, data))
+ except Exception: # pylint: disable=broad-except
+ self.logger.exception(
+ ("Exception caught while sending message to "
+ "controller %s: %s"),
+ type(self._handlers[message.namespace]).__name__,
+ _message_to_string(message, data))
+
+ else:
+ self.logger.debug(
+ "Received unknown namespace: %s",
+ _message_to_string(message, data))
+
+ def _cleanup(self):
+ """ Cleanup open channels and handlers """
+ for channel in self._open_channels:
+ try:
+ self.disconnect_channel(channel)
+ except Exception: # pylint: disable=broad-except
+ pass
+
+ for handler in self._handlers.values():
+ try:
+ handler.tear_down()
+ except Exception: # pylint: disable=broad-except
+ pass
+
+ self.socket.close()
+ self._report_connection_status(
+ ConnectionStatus(CONNECTION_STATUS_DISCONNECTED,
+ NetworkAddress(self.host, self.port)))
+ self.connecting = True
+
+ def _report_connection_status(self, status):
+ """ Report a change in the connection status to any listeners """
+ for listener in self._connection_listeners:
+ try:
+ self.logger.debug("connection listener: %x (%s)",
+ id(listener), type(listener).__name__)
+ listener.new_connection_status(status)
+ except Exception: # pylint: disable=broad-except
+ pass
+
+ def _read_bytes_from_socket(self, msglen):
+ """ Read bytes from the socket. """
+ chunks = []
+ bytes_recd = 0
+ while bytes_recd < msglen:
+ if self.stop.is_set():
+ raise InterruptLoop("Stopped while reading from socket")
+ try:
+ chunk = self.socket.recv(min(msglen - bytes_recd, 2048))
+ if chunk == b'':
+ raise socket.error("socket connection broken")
+ chunks.append(chunk)
+ bytes_recd += len(chunk)
+ except socket.timeout:
+ continue
+ except ssl.SSLError as exc:
+ # Support older ssl implementations which does not raise
+ # socket.timeout on timeouts
+ if _is_ssl_timeout(exc):
+ continue
+ raise
+ return b''.join(chunks)
+
+ def _read_message(self):
+ """ Reads a message from the socket and converts it to a message. """
+ # first 4 bytes is Big-Endian payload length
+ payload_info = self._read_bytes_from_socket(4)
+ read_len = unpack(">I", payload_info)[0]
+
+ # now read the payload
+ payload = self._read_bytes_from_socket(read_len)
+
+ # pylint: disable=no-member
+ message = cast_channel_pb2.CastMessage()
+ message.ParseFromString(payload)
+
+ return message
+
+ # pylint: disable=too-many-arguments
+ def send_message(self, destination_id, namespace, data,
+ inc_session_id=False, callback_function=False,
+ no_add_request_id=False, force=False):
+ """ Send a message to the Chromecast. """
+
+ # namespace is a string containing namespace
+ # data is a dict that will be converted to json
+ # wait_for_response only works if we have a request id
+
+ # If channel is not open yet, connect to it.
+ self._ensure_channel_connected(destination_id)
+
+ request_id = None
+ if not no_add_request_id:
+ request_id = self._gen_request_id()
+ data[REQUEST_ID] = request_id
+
+ if inc_session_id:
+ data[SESSION_ID] = self.session_id
+
+ # pylint: disable=no-member
+ msg = cast_channel_pb2.CastMessage()
+
+ msg.protocol_version = msg.CASTV2_1_0
+ msg.source_id = self.source_id
+ msg.destination_id = destination_id
+ msg.payload_type = cast_channel_pb2.CastMessage.STRING
+ msg.namespace = namespace
+ msg.payload_utf8 = _json_to_payload(data)
+
+ # prepend message with Big-Endian 4 byte payload size
+ be_size = pack(">I", msg.ByteSize())
+
+ # Log all messages except heartbeat
+ if msg.namespace != NS_HEARTBEAT:
+ self.logger.debug("Sending: %s", _message_to_string(msg, data))
+
+ if not force and self.stop.is_set():
+ raise PyChromecastStopped("Socket client's thread is stopped.")
+ if not self.connecting and not self._force_recon:
+ try:
+ if not no_add_request_id and callback_function:
+ self._request_callbacks[request_id] = {
+ 'event': threading.Event(),
+ 'response': None,
+ 'function': callback_function,
+ }
+ self.socket.sendall(be_size + msg.SerializeToString())
+ except socket.error:
+ self._request_callbacks.pop(request_id, None)
+ self._force_recon = True
+ self.logger.info('Error writing to socket.')
+ else:
+ raise NotConnected("Chromecast is connecting...")
+
+ def send_platform_message(self, namespace, message, inc_session_id=False,
+ callback_function_param=False):
+ """ Helper method to send a message to the platform. """
+ return self.send_message(PLATFORM_DESTINATION_ID, namespace, message,
+ inc_session_id, callback_function_param)
+
+ def send_app_message(self, namespace, message, inc_session_id=False,
+ callback_function_param=False):
+ """ Helper method to send a message to current running app. """
+ if namespace not in self.app_namespaces:
+ raise UnsupportedNamespace(
+ ("Namespace {} is not supported by current app. "
+ "Supported are {}").format(namespace,
+ ", ".join(self.app_namespaces)))
+
+ return self.send_message(self.destination_id, namespace, message,
+ inc_session_id, callback_function_param)
+
+ def register_connection_listener(self, listener):
+ """ Register a connection listener for when the socket connection
+ changes. Listeners will be called with
+ listener.new_connection_status(status) """
+ self._connection_listeners.append(listener)
+
+ def _ensure_channel_connected(self, destination_id):
+ """ Ensure we opened a channel to destination_id. """
+ if destination_id not in self._open_channels:
+ self._open_channels.append(destination_id)
+
+ self.send_message(
+ destination_id, NS_CONNECTION,
+ {MESSAGE_TYPE: TYPE_CONNECT,
+ 'origin': {},
+ 'userAgent': 'PyChromecast',
+ 'senderInfo': {
+ 'sdkType': 2,
+ 'version': '15.605.1.3',
+ 'browserVersion': "44.0.2403.30",
+ 'platform': 4,
+ 'systemVersion': 'Macintosh; Intel Mac OS X10_10_3',
+ 'connectionType': 1}},
+ no_add_request_id=True)
+
+ def disconnect_channel(self, destination_id):
+ """ Disconnect a channel with destination_id. """
+ if destination_id in self._open_channels:
+ self.send_message(
+ destination_id, NS_CONNECTION,
+ {MESSAGE_TYPE: TYPE_CLOSE, 'origin': {}},
+ no_add_request_id=True, force=True)
+
+ self._open_channels.remove(destination_id)
+
+ self.handle_channel_disconnected()
+
+ def handle_channel_disconnected(self):
+ """ Handles a channel being disconnected. """
+ for namespace in self.app_namespaces:
+ if namespace in self._handlers:
+ self._handlers[namespace].channel_disconnected()
+
+ self.app_namespaces = []
+ self.destination_id = None
+ self.session_id = None
+
+
+class ConnectionController(BaseController):
+ """ Controller to respond to connection messages. """
+
+ def __init__(self):
+ super(ConnectionController, self).__init__(NS_CONNECTION)
+
+ def receive_message(self, message, data):
+ """ Called when a connection message is received. """
+ if self._socket_client.is_stopped:
+ return True
+
+ if data[MESSAGE_TYPE] == TYPE_CLOSE:
+ # The cast device is asking us to acknowledge closing this channel.
+ self._socket_client.disconnect_channel(message.source_id)
+
+ # Schedule a status update so that a channel is created.
+ self._socket_client.receiver_controller.update_status()
+
+ return True
+
+ return False
+
+
+class HeartbeatController(BaseController):
+ """ Controller to respond to heartbeat messages. """
+
+ def __init__(self):
+ super(HeartbeatController, self).__init__(
+ NS_HEARTBEAT, target_platform=True)
+ self.last_ping = 0
+ self.last_pong = time.time()
+
+ def receive_message(self, message, data):
+ """ Called when a heartbeat message is received. """
+ if self._socket_client.is_stopped:
+ return True
+
+ if data[MESSAGE_TYPE] == TYPE_PING:
+ try:
+ self._socket_client.send_message(
+ PLATFORM_DESTINATION_ID, self.namespace,
+ {MESSAGE_TYPE: TYPE_PONG}, no_add_request_id=True)
+ except PyChromecastStopped:
+ self._socket_client.logger.debug(
+ "Heartbeat error when sending response, "
+ "Chromecast connection has stopped")
+
+ return True
+
+ elif data[MESSAGE_TYPE] == TYPE_PONG:
+ self.reset()
+ return True
+
+ return False
+
+ def ping(self):
+ """ Send a ping message. """
+ self.last_ping = time.time()
+ try:
+ self.send_message({MESSAGE_TYPE: TYPE_PING})
+ except NotConnected:
+ self._socket_client.logger.error("Chromecast is disconnected. " +
+ "Cannot ping until reconnected.")
+
+ def reset(self):
+ """ Reset expired counter. """
+ self.last_pong = time.time()
+
+ def is_expired(self):
+ """ Indicates if connection has expired. """
+ if time.time() - self.last_ping > HB_PING_TIME:
+ self.ping()
+
+ return (time.time() - self.last_pong) > HB_PING_TIME + HB_PONG_TIME
+
+
+class ReceiverController(BaseController):
+ """
+ Controller to interact with the Chromecast platform.
+
+ :param cast_type: Type of Chromecast device.
+ """
+
+ def __init__(self, cast_type=CAST_TYPE_CHROMECAST, blocking=True):
+ super(ReceiverController, self).__init__(
+ NS_RECEIVER, target_platform=True)
+
+ self.status = None
+ self.launch_failure = None
+ self.app_to_launch = None
+ self.cast_type = cast_type
+ self.blocking = blocking
+ self.app_launch_event = threading.Event()
+ self.app_launch_event_function = None
+
+ self._status_listeners = []
+ self._launch_error_listeners = []
+
+ @property
+ def app_id(self):
+ """ Convenience method to retrieve current app id. """
+ return self.status.app_id if self.status else None
+
+ def receive_message(self, message, data):
+ """ Called when a receiver-message has been received. """
+ if data[MESSAGE_TYPE] == TYPE_RECEIVER_STATUS:
+ self._process_get_status(data)
+
+ return True
+
+ elif data[MESSAGE_TYPE] == TYPE_LAUNCH_ERROR:
+ self._process_launch_error(data)
+
+ return True
+
+ return False
+
+ def register_status_listener(self, listener):
+ """ Register a status listener for when a new Chromecast status
+ has been received. Listeners will be called with
+ listener.new_cast_status(status) """
+ self._status_listeners.append(listener)
+
+ def register_launch_error_listener(self, listener):
+ """ Register a listener for when a new launch error message
+ has been received. Listeners will be called with
+ listener.new_launch_error(launch_failure) """
+ self._launch_error_listeners.append(listener)
+
+ def update_status(self, callback_function_param=False):
+ """ Sends a message to the Chromecast to update the status. """
+ self.logger.debug("Receiver:Updating status")
+ self.send_message({MESSAGE_TYPE: TYPE_GET_STATUS},
+ callback_function=callback_function_param)
+
+ def launch_app(self, app_id, force_launch=False, callback_function=False):
+ """ Launches an app on the Chromecast.
+
+ Will only launch if it is not currently running unless
+ force_launch=True. """
+
+ if not force_launch and self.app_id is None:
+ self.update_status(lambda response:
+ self._send_launch_message(app_id, force_launch,
+ callback_function))
+ else:
+ self._send_launch_message(app_id, force_launch, callback_function)
+
+ def _send_launch_message(self, app_id, force_launch=False,
+ callback_function=False):
+ if force_launch or self.app_id != app_id:
+ self.logger.info("Receiver:Launching app %s", app_id)
+
+ self.app_to_launch = app_id
+ self.app_launch_event.clear()
+ self.app_launch_event_function = callback_function
+ self.launch_failure = None
+
+ self.send_message({MESSAGE_TYPE: TYPE_LAUNCH,
+ APP_ID: app_id},
+ callback_function=lambda response:
+ self._block_till_launched(app_id))
+ else:
+ self.logger.info(
+ "Not launching app %s - already running", app_id)
+ if callback_function:
+ callback_function()
+
+ def _block_till_launched(self, app_id):
+ if self.blocking:
+ self.app_launch_event.wait()
+ if self.launch_failure:
+ raise LaunchError(
+ "Failed to launch app: {}, Reason: {}".format(
+ app_id, self.launch_failure.reason))
+
+ def stop_app(self, callback_function_param=False):
+ """ Stops the current running app on the Chromecast. """
+ self.logger.info("Receiver:Stopping current app '%s'", self.app_id)
+ return self.send_message(
+ {MESSAGE_TYPE: 'STOP'},
+ inc_session_id=True, callback_function=callback_function_param)
+
+ def set_volume(self, volume):
+ """ Allows to set volume. Should be value between 0..1.
+ Returns the new volume.
+
+ """
+ volume = min(max(0, volume), 1)
+ self.logger.info("Receiver:setting volume to %.1f", volume)
+ self.send_message({MESSAGE_TYPE: 'SET_VOLUME',
+ 'volume': {'level': volume}})
+ return volume
+
+ def set_volume_muted(self, muted):
+ """ Allows to mute volume. """
+ self.send_message(
+ {MESSAGE_TYPE: 'SET_VOLUME',
+ 'volume': {'muted': muted}})
+
+ @staticmethod
+ def _parse_status(data, cast_type):
+ """
+ Parses a STATUS message and returns a CastStatus object.
+
+ :type data: dict
+ :param cast_type: Type of Chromecast.
+ :rtype: CastStatus
+ """
+ data = data.get('status', {})
+
+ volume_data = data.get('volume', {})
+
+ try:
+ app_data = data['applications'][0]
+ except KeyError:
+ app_data = {}
+
+ is_audio = cast_type in (CAST_TYPE_AUDIO, CAST_TYPE_GROUP)
+
+ status = CastStatus(
+ data.get('isActiveInput', None if is_audio else False),
+ data.get('isStandBy', None if is_audio else True),
+ volume_data.get('level', 1.0),
+ volume_data.get('muted', False),
+ app_data.get(APP_ID),
+ app_data.get('displayName'),
+ [item['name'] for item in app_data.get('namespaces', [])],
+ app_data.get(SESSION_ID),
+ app_data.get('transportId'),
+ app_data.get('statusText', '')
+ )
+ return status
+
+ def _process_get_status(self, data):
+ """ Processes a received STATUS message and notifies listeners. """
+ status = self._parse_status(data, self.cast_type)
+ is_new_app = self.app_id != status.app_id and self.app_to_launch
+ self.status = status
+
+ self.logger.debug("Received status: %s", self.status)
+ self._report_status()
+
+ if is_new_app and self.app_to_launch == self.app_id:
+ self.app_to_launch = None
+ self.app_launch_event.set()
+ if self.app_launch_event_function:
+ self.logger.debug("Start app_launch_event_function...")
+ self.app_launch_event_function()
+ self.app_launch_event_function = None
+
+ def _report_status(self):
+ """ Reports the current status to all listeners. """
+ for listener in self._status_listeners:
+ try:
+ listener.new_cast_status(self.status)
+ except Exception: # pylint: disable=broad-except
+ pass
+
+ @staticmethod
+ def _parse_launch_error(data):
+ """
+ Parses a LAUNCH_ERROR message and returns a LaunchFailure object.
+
+ :type data: dict
+ :rtype: LaunchFailure
+ """
+ return LaunchFailure(
+ data.get(ERROR_REASON, None),
+ data.get(APP_ID),
+ data.get(REQUEST_ID),
+ )
+
+ def _process_launch_error(self, data):
+ """
+ Processes a received LAUNCH_ERROR message and notifies listeners.
+ """
+ launch_failure = self._parse_launch_error(data)
+ self.launch_failure = launch_failure
+
+ if self.app_to_launch:
+ self.app_to_launch = None
+ self.app_launch_event.set()
+
+ self.logger.debug("Launch status: %s", launch_failure)
+
+ for listener in self._launch_error_listeners:
+ try:
+ listener.new_launch_error(launch_failure)
+ except Exception: # pylint: disable=broad-except
+ pass
+
+ def tear_down(self):
+ """ Called when controller is destroyed. """
+ super(ReceiverController, self).tear_down()
+
+ self.status = None
+ self.launch_failure = None
+ self.app_to_launch = None
+ self.app_launch_event.clear()
+ self._report_status()
+
+ self._status_listeners[:] = []
+
+
+def new_socket():
+ """
+ Create a new socket with OS-specific parameters
+
+ Try to set SO_REUSEPORT for BSD-flavored systems if it's an option.
+ Catches errors if not.
+ """
+ new_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ new_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+ try:
+ # noinspection PyUnresolvedReferences
+ reuseport = socket.SO_REUSEPORT
+ except AttributeError:
+ pass
+ else:
+ try:
+ new_sock.setsockopt(socket.SOL_SOCKET, reuseport, 1)
+ except (OSError, socket.error) as err:
+ # OSError on python 3, socket.error on python 2
+ if err.errno != errno.ENOPROTOOPT:
+ raise
+
+ return new_sock
diff --git a/resources/pychromecast/pylintrc b/resources/pychromecast/pylintrc
new file mode 100644
index 0000000..75ea717
--- /dev/null
+++ b/resources/pychromecast/pylintrc
@@ -0,0 +1,8 @@
+[MASTER]
+ignore=cast_channel_pb2.py,authority_keys_pb2.py,logging_pb2.py
+reports=no
+
+disable=locally-disabled
+
+[EXCEPTIONS]
+overgeneral-exceptions=Exception,PyChromecastError
diff --git a/resources/pychromecast/requirements.txt b/resources/pychromecast/requirements.txt
new file mode 100644
index 0000000..ab4e266
--- /dev/null
+++ b/resources/pychromecast/requirements.txt
@@ -0,0 +1,3 @@
+requests>=2.0
+protobuf>=3.0.0
+zeroconf>=0.17.7
diff --git a/resources/pychromecast/setup.cfg b/resources/pychromecast/setup.cfg
new file mode 100644
index 0000000..5e40900
--- /dev/null
+++ b/resources/pychromecast/setup.cfg
@@ -0,0 +1,2 @@
+[wheel]
+universal = 1
diff --git a/resources/pychromecast/setup.py b/resources/pychromecast/setup.py
new file mode 100644
index 0000000..35f0946
--- /dev/null
+++ b/resources/pychromecast/setup.py
@@ -0,0 +1,29 @@
+from setuptools import setup, find_packages
+
+
+long_description = open('README.rst').read()
+
+setup(
+ name='PyChromecast',
+ version='2.1.0',
+ license='MIT',
+ url='https://github.com/balloob/pychromecast',
+ author='Paulus Schoutsen',
+ author_email='paulus@paulusschoutsen.nl',
+ description='Python module to talk to Google Chromecast.',
+ long_description=long_description,
+ packages=find_packages(),
+ zip_safe=False,
+ include_package_data=True,
+ platforms='any',
+ install_requires=list(val.strip() for val in open('requirements.txt')),
+ classifiers=[
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 3',
+ 'Topic :: Software Development :: Libraries :: Python Modules'
+ ]
+)