From b799d63e67307abdfe6ccf94979d4b25ac642d2d Mon Sep 17 00:00:00 2001 From: jodeko Date: Tue, 24 Jan 2017 06:50:49 +0100 Subject: [PATCH] first commit --- LICENSE | 661 +++++ README.md | 4 + conf/nginx.conf | 8 + manifest.json | 55 + scripts/backup | 18 + scripts/install | 58 + scripts/remove | 34 + scripts/restore | 35 + scripts/upgrade | 45 + sources/3rdparty/favico-0.3.5.min.js | 7 + sources/3rdparty/inflate.min.js | 15 + sources/3rdparty/inflate.min.js.map | 8 + sources/COPYING | 674 ++++++ sources/README.md | 92 + sources/assets/audio/sonar.mp3 | Bin 0 -> 10368 bytes sources/assets/audio/sonar.ogg | Bin 0 -> 20011 bytes sources/assets/img/badge_firefoxos.png | Bin 0 -> 4293 bytes sources/assets/img/badge_playstore.png | Bin 0 -> 8913 bytes sources/assets/img/favicon.png | Bin 0 -> 1991 bytes sources/assets/img/glowing-bear.png | Bin 0 -> 57024 bytes sources/assets/img/glowing-bear.svg | 1 + sources/assets/img/glowing_bear_128x128.png | Bin 0 -> 12309 bytes sources/assets/img/glowing_bear_60x60.png | Bin 0 -> 4883 bytes sources/assets/img/glowing_bear_90x90.png | Bin 0 -> 9713 bytes .../assets/img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes sources/bower.json | 17 + sources/css/glowingbear.css | 852 +++++++ sources/css/themes/black.css | 22 + sources/css/themes/dark.css | 2126 +++++++++++++++++ sources/css/themes/light.css | 2102 ++++++++++++++++ sources/directives/input.html | 14 + sources/directives/plugin.html | 18 + sources/index.html | 513 ++++ sources/js/connection.js | 387 +++ sources/js/file-change.js | 23 + sources/js/filters.js | 205 ++ sources/js/glowingbear.js | 848 +++++++ sources/js/handlers.js | 451 ++++ sources/js/imgur-drop-directive.js | 49 + sources/js/imgur.js | 128 + sources/js/inputbar.js | 489 ++++ sources/js/irc-utils.js | 228 ++ sources/js/localstorage.js | 117 + sources/js/models.js | 596 +++++ sources/js/notifications.js | 210 ++ sources/js/plugin-directive.js | 86 + sources/js/plugins.js | 530 ++++ sources/js/settings.js | 82 + sources/js/utils.js | 29 + sources/js/websockets.js | 150 ++ sources/js/weechat.js | 1284 ++++++++++ sources/js/whenscrolled-directive.js | 21 + sources/manifest.json | 25 + sources/manifest.webapp | 29 + sources/package.json | 36 + sources/run_tests.sh | 2 + sources/serviceworker.js | 46 + sources/test/e2e/scenarios.js | 26 + sources/test/karma.conf.js | 49 + sources/test/protractor-conf.js | 19 + sources/test/unit/filters.js | 95 + sources/test/unit/plugins.js | 169 ++ sources/webapp.manifest.json | 33 + 63 files changed, 13821 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 conf/nginx.conf create mode 100644 manifest.json create mode 100755 scripts/backup create mode 100755 scripts/install create mode 100755 scripts/remove create mode 100755 scripts/restore create mode 100755 scripts/upgrade create mode 100644 sources/3rdparty/favico-0.3.5.min.js create mode 100644 sources/3rdparty/inflate.min.js create mode 100644 sources/3rdparty/inflate.min.js.map create mode 100644 sources/COPYING create mode 100644 sources/README.md create mode 100644 sources/assets/audio/sonar.mp3 create mode 100644 sources/assets/audio/sonar.ogg create mode 100644 sources/assets/img/badge_firefoxos.png create mode 100644 sources/assets/img/badge_playstore.png create mode 100644 sources/assets/img/favicon.png create mode 100644 sources/assets/img/glowing-bear.png create mode 100644 sources/assets/img/glowing-bear.svg create mode 100644 sources/assets/img/glowing_bear_128x128.png create mode 100644 sources/assets/img/glowing_bear_60x60.png create mode 100644 sources/assets/img/glowing_bear_90x90.png create mode 100644 sources/assets/img/glyphicons-halflings-white.png create mode 100644 sources/bower.json create mode 100644 sources/css/glowingbear.css create mode 100644 sources/css/themes/black.css create mode 100644 sources/css/themes/dark.css create mode 100644 sources/css/themes/light.css create mode 100644 sources/directives/input.html create mode 100644 sources/directives/plugin.html create mode 100644 sources/index.html create mode 100644 sources/js/connection.js create mode 100644 sources/js/file-change.js create mode 100644 sources/js/filters.js create mode 100644 sources/js/glowingbear.js create mode 100644 sources/js/handlers.js create mode 100644 sources/js/imgur-drop-directive.js create mode 100644 sources/js/imgur.js create mode 100644 sources/js/inputbar.js create mode 100644 sources/js/irc-utils.js create mode 100644 sources/js/localstorage.js create mode 100644 sources/js/models.js create mode 100644 sources/js/notifications.js create mode 100644 sources/js/plugin-directive.js create mode 100644 sources/js/plugins.js create mode 100644 sources/js/settings.js create mode 100644 sources/js/utils.js create mode 100644 sources/js/websockets.js create mode 100644 sources/js/weechat.js create mode 100644 sources/js/whenscrolled-directive.js create mode 100644 sources/manifest.json create mode 100644 sources/manifest.webapp create mode 100644 sources/package.json create mode 100755 sources/run_tests.sh create mode 100644 sources/serviceworker.js create mode 100644 sources/test/e2e/scenarios.js create mode 100644 sources/test/karma.conf.js create mode 100644 sources/test/protractor-conf.js create mode 100644 sources/test/unit/filters.js create mode 100644 sources/test/unit/plugins.js create mode 100644 sources/webapp.manifest.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dba13ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + 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 +them 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..900134d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +Glowing Bear for YunoHost +------------------------- + +https://www.glowing-bear.org/ diff --git a/conf/nginx.conf b/conf/nginx.conf new file mode 100644 index 0000000..f65be88 --- /dev/null +++ b/conf/nginx.conf @@ -0,0 +1,8 @@ +location YNH_WWW_PATH { + + # Path to source + alias YNH_WWW_ALIAS ; + + # Include SSOWAT user panel. + include conf.d/yunohost_panel.conf.inc; +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..f9b19ae --- /dev/null +++ b/manifest.json @@ -0,0 +1,55 @@ +{ + "name": "Glowing Bear", + "id": "glowingbear", + "packaging_format": 1, + "description": { + "en": "A web client for WeeChat.", + "fr": "Un client Web pour WeeChat." + }, + "url": "https://www.glowing-bear.org", + "license": "free", + "version": "0.6.0", + "maintainer": { + "name": "jodeko", + "email": "jodeko@riseup.net" + }, + "requirements": { + "yunohost": ">> 2.4.0" + }, + "multi_instance": true, + "services": [ + "nginx" + ], + "arguments": { + "install" : [ + { + "name": "domain", + "type": "domain", + "ask": { + "en": "Choose a domain name for ynhexample", + "fr": "Choisissez un nom de domaine pour ynhexample" + }, + "example": "example.com" + }, + { + "name": "path", + "type": "path", + "ask": { + "en": "Choose a path for your WebApp", + "fr": "Choisissez un chemin pour votre WebApp" + }, + "example": "/glowing-bear", + "default": "/glowing-bear" + }, + { + "name": "is_public", + "type": "boolean", + "ask": { + "en": "Is it a public website?", + "fr": "Est-ce un site public ?" + }, + "default": false + } + ] + } +} diff --git a/scripts/backup b/scripts/backup new file mode 100755 index 0000000..f944306 --- /dev/null +++ b/scripts/backup @@ -0,0 +1,18 @@ +#!/bin/bash + +# Exit on command errors and treat unset variables as an error +set -eu + +# See comments in install script +app=$YNH_APP_INSTANCE_NAME + +# Source YunoHost helpers +source /usr/share/yunohost/helpers + +# Backup sources & data +# Note: the last argument is where to save this path, see the restore script. +ynh_backup "/var/www/${app}" "sources" + +# Copy NGINX configuration +domain=$(ynh_app_setting_get "$app" domain) +ynh_backup "/etc/nginx/conf.d/${domain}.d/${app}.conf" "nginx.conf" diff --git a/scripts/install b/scripts/install new file mode 100755 index 0000000..170372c --- /dev/null +++ b/scripts/install @@ -0,0 +1,58 @@ +#!/bin/bash + +# Exit on command errors and treat unset variables as an error +set -eu + +# This is a multi-instance app, meaning it can be installed several times independently +# The id of the app as stated in the manifest is available as $YNH_APP_ID +# The instance number is available as $YNH_APP_INSTANCE_NUMBER (equals "1", "2", ...) +# The app instance name is available as $YNH_APP_INSTANCE_NAME +# - the first time the app is installed, YNH_APP_INSTANCE_NAME = ynhexample +# - the second time the app is installed, YNH_APP_INSTANCE_NAME = ynhexample__2 +# - ynhexample__{N} for the subsequent installations, with N=3,4, ... +# The app instance name is probably what you are interested the most, since this is +# guaranteed to be unique. This is a good unique identifier to define installation path, +# db names, ... +app=$YNH_APP_INSTANCE_NAME + +# Retrieve arguments +domain=$YNH_APP_ARG_DOMAIN +path=$YNH_APP_ARG_PATH +is_public=$YNH_APP_ARG_IS_PUBLIC + +# Source YunoHost helpers +source /usr/share/yunohost/helpers + +# Save app settings +ynh_app_setting_set "$app" is_public "$is_public" + +# Check domain/path availability +sudo yunohost app checkurl "${domain}${path}" -a "$app" \ + || ynh_die "Path not available: ${domain}${path}" + +# Copy source files +src_path=/var/www/$app +sudo mkdir -p $src_path +sudo cp -a ../sources/. $src_path + +# Set permissions to app files +# you may need to make some file and/or directory writeable by www-data (nginx user) +sudo chown -R root: $src_path + +# Modify Nginx configuration file and copy it to Nginx conf directory +nginx_conf=../conf/nginx.conf +sed -i "s@YNH_WWW_PATH@$path@g" $nginx_conf +sed -i "s@YNH_WWW_ALIAS@$src_path/@g" $nginx_conf +# If a dedicated php-fpm process is used: +# Don't forget to modify ../conf/nginx.conf accordingly or your app will not work! +# sed -i "s@YNH_WWW_APP@$app@g" $nginx_conf +sudo cp $nginx_conf /etc/nginx/conf.d/$domain.d/$app.conf + +# If app is public, add url to SSOWat conf as skipped_uris +if [[ $is_public -eq 1 ]]; then + # unprotected_uris allows SSO credentials to be passed anyway. + ynh_app_setting_set "$app" unprotected_uris "/" +fi + +# Reload services +sudo service nginx reload diff --git a/scripts/remove b/scripts/remove new file mode 100755 index 0000000..59ef331 --- /dev/null +++ b/scripts/remove @@ -0,0 +1,34 @@ +#!/bin/bash + +# See comments in install script +app=$YNH_APP_INSTANCE_NAME + +# Source YunoHost helpers +source /usr/share/yunohost/helpers + +# Retrieve app settings +domain=$(ynh_app_setting_get "$app" domain) + +# Remove sources +sudo rm -rf /var/www/$app + +# Remove nginx configuration file +sudo rm -f /etc/nginx/conf.d/$domain.d/$app.conf + +### PHP (remove if not used) ### +# If a dedicated php-fpm process is used: +# sudo rm -f /etc/php5/fpm/pool.d/$app.conf +# sudo service php5-fpm reload +### PHP end ### + +### MySQL (remove if not used) ### +# If a MySQL database is used: +# # Drop MySQL database and user +# dbname=$app +# dbuser=$app +# ynh_mysql_drop_db "$dbname" || true +# ynh_mysql_drop_user "$dbuser" || true +### MySQL end ### + +# Reload nginx service +sudo service nginx reload diff --git a/scripts/restore b/scripts/restore new file mode 100755 index 0000000..3247df4 --- /dev/null +++ b/scripts/restore @@ -0,0 +1,35 @@ +#!/bin/bash + +# Note: each files and directories you've saved using the ynh_backup helper +# will be located in the current directory, regarding the last argument. + +# Exit on command errors and treat unset variables as an error +set -eu + +# See comments in install script +app=$YNH_APP_INSTANCE_NAME + +# Source YunoHost helpers +source /usr/share/yunohost/helpers + +# Retrieve old app settings +domain=$(ynh_app_setting_get "$app" domain) +path=$(ynh_app_setting_get "$app" path) + +# Check domain/path availability +sudo yunohost app checkurl "${domain}${path}" -a "$app" \ + || ynh_die "Path not available: ${domain}${path}" + +# Restore sources & data +src_path="/var/www/${app}" +sudo cp -a ./sources "$src_path" + +# Restore permissions to app files +# you may need to make some file and/or directory writeable by www-data (nginx user) +sudo chown -R root: "$src_path" + +# Restore NGINX configuration +sudo cp -a ./nginx.conf "/etc/nginx/conf.d/${domain}.d/${app}.conf" + +# Restart webserver +sudo service nginx reload diff --git a/scripts/upgrade b/scripts/upgrade new file mode 100755 index 0000000..3ead2fd --- /dev/null +++ b/scripts/upgrade @@ -0,0 +1,45 @@ +#!/bin/bash + +# Exit on command errors and treat unset variables as an error +set -eu + +# See comments in install script +app=$YNH_APP_INSTANCE_NAME + +# Source YunoHost helpers +source /usr/share/yunohost/helpers + +# Retrieve app settings +domain=$(ynh_app_setting_get "$app" domain) +path=$(ynh_app_setting_get "$app" path) +is_public=$(ynh_app_setting_get "$app" is_public) + +# Remove trailing "/" for next commands +path=${path%/} + +# Copy source files +src_path=/var/www/$app +sudo mkdir -p $src_path +sudo cp -a ../sources/. $src_path + +# Set permissions to app files +# you may need to make some file and/or directory writeable by www-data (nginx user) +sudo chown -R root: $src_path + +# Modify Nginx configuration file and copy it to Nginx conf directory +nginx_conf=../conf/nginx.conf +sed -i "s@YNH_WWW_PATH@$path@g" $nginx_conf +sed -i "s@YNH_WWW_ALIAS@$src_path/@g" $nginx_conf +# If a dedicated php-fpm process is used: +# +# sed -i "s@YNH_WWW_APP@$app@g" $nginx_conf +sudo cp $nginx_conf /etc/nginx/conf.d/$domain.d/$app.conf + +# If app is public, add url to SSOWat conf as skipped_uris +if [[ $is_public -eq 1 ]]; then + # See install script + ynh_app_setting_set "$app" unprotected_uris "/" +fi + +# Reload nginx service +sudo service nginx reload diff --git a/sources/3rdparty/favico-0.3.5.min.js b/sources/3rdparty/favico-0.3.5.min.js new file mode 100644 index 0000000..c4630af --- /dev/null +++ b/sources/3rdparty/favico-0.3.5.min.js @@ -0,0 +1,7 @@ +/** + * @license MIT + * @fileOverview Favico animations + * @author Miroslav Magda, http://blog.ejci.net + * @version 0.3.5 + */ +!function(){var e=function(e){"use strict";function t(e){if(e.paused||e.ended||w)return!1;try{l.clearRect(0,0,s,h),l.drawImage(e,0,0,s,h)}catch(o){}p=setTimeout(t,O.duration,e),L.setIcon(c)}function o(e){var t=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;e=e.replace(t,function(e,t,o,n){return t+t+o+o+n+n});var o=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return o?{r:parseInt(o[1],16),g:parseInt(o[2],16),b:parseInt(o[3],16)}:!1}function n(e,t){var o,n={};for(o in e)n[o]=e[o];for(o in t)n[o]=t[o];return n}function i(){return document.hidden||document.msHidden||document.webkitHidden||document.mozHidden}e=e?e:{};var r,a,h,s,c,l,f,d,u,y,g,w,m,x,p,b={bgColor:"#d00",textColor:"#fff",fontFamily:"sans-serif",fontStyle:"bold",type:"circle",position:"down",animation:"slide",elementId:!1};m={},m.ff="undefined"!=typeof InstallTrigger,m.chrome=!!window.chrome,m.opera=!!window.opera||navigator.userAgent.indexOf("Opera")>=0,m.ie=/*@cc_on!@*/!1,m.safari=Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0,m.supported=m.chrome||m.ff||m.opera;var v=[];g=function(){},d=w=!1;var C=function(){r=n(b,e),r.bgColor=o(r.bgColor),r.textColor=o(r.textColor),r.position=r.position.toLowerCase(),r.animation=O.types[""+r.animation]?r.animation:b.animation;var t=r.position.indexOf("up")>-1,i=r.position.indexOf("left")>-1;if(t||i)for(var d=0;d0?f.height:32,s=f.width>0?f.width:32,c.height=h,c.width=s,l=c.getContext("2d"),M.ready()}):(f.setAttribute("src",""),h=32,s=32,f.height=h,f.width=s,c.height=h,c.width=s,l=c.getContext("2d"),M.ready())}catch(y){throw"Error initializing favico. Message: "+y.message}},M={};M.ready=function(){d=!0,M.reset(),g()},M.reset=function(){d&&(v=[],u=!1,l.clearRect(0,0,s,h),l.drawImage(f,0,0,s,h),L.setIcon(c),window.clearTimeout(x),window.clearTimeout(p))},M.start=function(){if(d&&!y){var e=function(){u=v[0],y=!1,v.length>0&&(v.shift(),M.start())};if(v.length>0){y=!0;var t=function(){["type","animation","bgColor","textColor","fontFamily","fontStyle"].forEach(function(e){e in v[0].options&&(r[e]=v[0].options[e])}),O.run(v[0].options,function(){e()},!1)};u?O.run(u.options,function(){t()},!0):t()}}};var I={},E=function(e){return e.n="number"==typeof e.n?Math.abs(0|e.n):e.n,e.x=s*e.x,e.y=h*e.y,e.w=s*e.w,e.h=h*e.h,e.len=(""+e.n).length,e};I.circle=function(e){e=E(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),l.clearRect(0,0,s,h),l.drawImage(f,0,0,s,h),l.beginPath(),l.font=r.fontStyle+" "+Math.floor(e.h*(e.n>99?.85:1))+"px "+r.fontFamily,l.textAlign="center",t?(l.moveTo(e.x+e.w/2,e.y),l.lineTo(e.x+e.w-e.h/2,e.y),l.quadraticCurveTo(e.x+e.w,e.y,e.x+e.w,e.y+e.h/2),l.lineTo(e.x+e.w,e.y+e.h-e.h/2),l.quadraticCurveTo(e.x+e.w,e.y+e.h,e.x+e.w-e.h/2,e.y+e.h),l.lineTo(e.x+e.h/2,e.y+e.h),l.quadraticCurveTo(e.x,e.y+e.h,e.x,e.y+e.h-e.h/2),l.lineTo(e.x,e.y+e.h/2),l.quadraticCurveTo(e.x,e.y,e.x+e.h/2,e.y)):l.arc(e.x+e.w/2,e.y+e.h/2,e.h/2,0,2*Math.PI),l.fillStyle="rgba("+r.bgColor.r+","+r.bgColor.g+","+r.bgColor.b+","+e.o+")",l.fill(),l.closePath(),l.beginPath(),l.stroke(),l.fillStyle="rgba("+r.textColor.r+","+r.textColor.g+","+r.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?l.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):l.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),l.closePath()},I.rectangle=function(e){e=E(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),l.clearRect(0,0,s,h),l.drawImage(f,0,0,s,h),l.beginPath(),l.font=r.fontStyle+" "+Math.floor(e.h*(e.n>99?.9:1))+"px "+r.fontFamily,l.textAlign="center",l.fillStyle="rgba("+r.bgColor.r+","+r.bgColor.g+","+r.bgColor.b+","+e.o+")",l.fillRect(e.x,e.y,e.w,e.h),l.fillStyle="rgba("+r.textColor.r+","+r.textColor.g+","+r.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?l.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):l.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),l.closePath()};var T=function(e,t){t=("string"==typeof t?{animation:t}:t)||{},g=function(){try{if("number"==typeof e?e>0:""!==e){var n={type:"badge",options:{n:e}};if("animation"in t&&O.types[""+t.animation]&&(n.options.animation=""+t.animation),"type"in t&&I[""+t.type]&&(n.options.type=""+t.type),["bgColor","textColor"].forEach(function(e){e in t&&(n.options[e]=o(t[e]))}),["fontStyle","fontFamily"].forEach(function(e){e in t&&(n.options[e]=t[e])}),v.push(n),v.length>100)throw"Too many badges requests in queue.";M.start()}else M.reset()}catch(i){throw"Error setting badge. Message: "+i.message}},d&&g()},A=function(e){g=function(){try{var t=e.width,o=e.height,n=document.createElement("img"),i=o/h>t/s?t/s:o/h;n.setAttribute("src",e.getAttribute("src")),n.height=o/i,n.width=t/i,l.clearRect(0,0,s,h),l.drawImage(n,0,0,s,h),L.setIcon(c)}catch(r){throw"Error setting image. Message: "+r.message}},d&&g()},U=function(e){g=function(){try{if("stop"===e)return w=!0,M.reset(),w=!1,void 0;e.addEventListener("play",function(){t(this)},!1)}catch(o){throw"Error setting video. Message: "+o.message}},d&&g()},R=function(e){if(window.URL&&window.URL.createObjectURL||(window.URL=window.URL||{},window.URL.createObjectURL=function(e){return e}),m.supported){var o=!1;navigator.getUserMedia=navigator.getUserMedia||navigator.oGetUserMedia||navigator.msGetUserMedia||navigator.mozGetUserMedia||navigator.webkitGetUserMedia,g=function(){try{if("stop"===e)return w=!0,M.reset(),w=!1,void 0;o=document.createElement("video"),o.width=s,o.height=h,navigator.getUserMedia({video:!0,audio:!1},function(e){o.src=URL.createObjectURL(e),o.play(),t(o)},function(){})}catch(n){throw"Error setting webcam. Message: "+n.message}},d&&g()}},L={};L.getIcon=function(){var e=!1,t="",o=function(){for(var e=document.getElementsByTagName("head")[0].getElementsByTagName("link"),t=e.length,o=t-1;o>=0;o--)if(/(^|\s)icon(\s|$)/i.test(e[o].getAttribute("rel")))return e[o];return!1};if(r.elementId?(e=document.getElementById(r.elementId),e.setAttribute("href",e.getAttribute("src"))):(e=o(),e===!1&&(e=document.createElement("link"),e.setAttribute("rel","icon"),document.getElementsByTagName("head")[0].appendChild(e))),t=r.elementId?e.src:e.href,"data:"!==t.substr(0,5)&&-1===t.indexOf(document.location.hostname))throw new Error("Error setting favicon. Favicon image is on different domain (Icon: "+t+", Domain: "+document.location.hostname+")");return e.setAttribute("type","image/png"),e},L.setIcon=function(e){var t=e.toDataURL("image/png");if(r.elementId)document.getElementById(r.elementId).setAttribute("src",t);else if(m.ff||m.opera){var o=a;a=document.createElement("link"),m.opera&&a.setAttribute("rel","icon"),a.setAttribute("rel","icon"),a.setAttribute("type","image/png"),document.getElementsByTagName("head")[0].appendChild(a),a.setAttribute("href",t),o.parentNode&&o.parentNode.removeChild(o)}else a.setAttribute("href",t)};var O={};return O.duration=40,O.types={},O.types.fade=[{x:.4,y:.4,w:.6,h:.6,o:0},{x:.4,y:.4,w:.6,h:.6,o:.1},{x:.4,y:.4,w:.6,h:.6,o:.2},{x:.4,y:.4,w:.6,h:.6,o:.3},{x:.4,y:.4,w:.6,h:.6,o:.4},{x:.4,y:.4,w:.6,h:.6,o:.5},{x:.4,y:.4,w:.6,h:.6,o:.6},{x:.4,y:.4,w:.6,h:.6,o:.7},{x:.4,y:.4,w:.6,h:.6,o:.8},{x:.4,y:.4,w:.6,h:.6,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],O.types.none=[{x:.4,y:.4,w:.6,h:.6,o:1}],O.types.pop=[{x:1,y:1,w:0,h:0,o:1},{x:.9,y:.9,w:.1,h:.1,o:1},{x:.8,y:.8,w:.2,h:.2,o:1},{x:.7,y:.7,w:.3,h:.3,o:1},{x:.6,y:.6,w:.4,h:.4,o:1},{x:.5,y:.5,w:.5,h:.5,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],O.types.popFade=[{x:.75,y:.75,w:0,h:0,o:0},{x:.65,y:.65,w:.1,h:.1,o:.2},{x:.6,y:.6,w:.2,h:.2,o:.4},{x:.55,y:.55,w:.3,h:.3,o:.6},{x:.5,y:.5,w:.4,h:.4,o:.8},{x:.45,y:.45,w:.5,h:.5,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],O.types.slide=[{x:.4,y:1,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.8,w:.6,h:.6,o:1},{x:.4,y:.7,w:.6,h:.6,o:1},{x:.4,y:.6,w:.6,h:.6,o:1},{x:.4,y:.5,w:.6,h:.6,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],O.run=function(e,t,o,a){var h=O.types[i()?"none":r.animation];return a=o===!0?"undefined"!=typeof a?a:h.length-1:"undefined"!=typeof a?a:0,t=t?t:function(){},a=0?(I[r.type](n(e,h[a])),x=setTimeout(function(){o?a-=1:a+=1,O.run(e,t,o,a)},O.duration),L.setIcon(c),void 0):(t(),void 0)},C(),{badge:T,video:U,image:A,webcam:R,reset:M.reset,browser:{supported:m.supported}}};"undefined"!=typeof define&&define.amd?define([],function(){return e}):"undefined"!=typeof module&&module.exports?module.exports=e:this.Favico=e}(); \ No newline at end of file diff --git a/sources/3rdparty/inflate.min.js b/sources/3rdparty/inflate.min.js new file mode 100644 index 0000000..3f0e3be --- /dev/null +++ b/sources/3rdparty/inflate.min.js @@ -0,0 +1,15 @@ +/** @license zlib.js 2012 - imaya [ https://github.com/imaya/zlib.js ] The MIT License */(function() {'use strict';var m=this;function q(c,d){var a=c.split("."),b=m;!(a[0]in b)&&b.execScript&&b.execScript("var "+a[0]);for(var e;a.length&&(e=a.shift());)!a.length&&void 0!==d?b[e]=d:b=b[e]?b[e]:b[e]={}};var s="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array&&"undefined"!==typeof DataView;function t(c){var d=c.length,a=0,b=Number.POSITIVE_INFINITY,e,f,g,h,k,l,p,n,r,K;for(n=0;na&&(a=c[n]),c[n]>=1;K=g<<16|n;for(r=l;r>>=1;switch(c){case 0:var d=this.input,a=this.a,b=this.c,e=this.b,f=d.length,g=void 0,h=void 0,k=b.length,l=void 0;this.d=this.f=0;if(a+1>=f)throw Error("invalid uncompressed block header: LEN");g=d[a++]|d[a++]<<8;if(a+1>=f)throw Error("invalid uncompressed block header: NLEN");h=d[a++]|d[a++]<<8;if(g===~h)throw Error("invalid uncompressed block header: length verify");if(a+g>d.length)throw Error("input buffer is broken");switch(this.i){case w:for(;e+ +g>b.length;){l=k-e;g-=l;if(s)b.set(d.subarray(a,a+l),e),e+=l,a+=l;else for(;l--;)b[e++]=d[a++];this.b=e;b=this.e();e=this.b}break;case v:for(;e+g>b.length;)b=this.e({p:2});break;default:throw Error("invalid inflate mode");}if(s)b.set(d.subarray(a,a+g),e),e+=g,a+=g;else for(;g--;)b[e++]=d[a++];this.a=a;this.b=e;this.c=b;break;case 1:this.j(z,A);break;case 2:B(this);break;default:throw Error("unknown BTYPE: "+c);}}return this.n()}; +var C=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],D=s?new Uint16Array(C):C,E=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,258,258],F=s?new Uint16Array(E):E,G=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0],H=s?new Uint8Array(G):G,I=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],J=s?new Uint16Array(I):I,L=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13, +13],M=s?new Uint8Array(L):L,N=new (s?Uint8Array:Array)(288),O,P;O=0;for(P=N.length;O=O?8:255>=O?9:279>=O?7:8;var z=t(N),Q=new (s?Uint8Array:Array)(30),R,S;R=0;for(S=Q.length;R=g)throw Error("input buffer is broken");a|=e[f++]<>>d;c.d=b-d;c.a=f;return h} +function T(c,d){for(var a=c.f,b=c.d,e=c.input,f=c.a,g=e.length,h=d[0],k=d[1],l,p;b=g);)a|=e[f++]<>>16;c.f=a>>p;c.d=b-p;c.a=f;return l&65535} +function B(c){function d(a,c,b){var d,e=this.q,f,g;for(g=0;gf)b>=e&&(this.b=b,a=this.e(),b=this.b),a[b++]=f;else{g=f-257;k=F[g];0=e&&(this.b=b,a=this.e(),b=this.b);for(;k--;)a[b]=a[b++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=b}; +u.prototype.z=function(c,d){var a=this.c,b=this.b;this.o=c;for(var e=a.length,f,g,h,k;256!==(f=T(this,c));)if(256>f)b>=e&&(a=this.e(),e=a.length),a[b++]=f;else{g=f-257;k=F[g];0e&&(a=this.e(),e=a.length);for(;k--;)a[b]=a[b++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=b}; +u.prototype.e=function(){var c=new (s?Uint8Array:Array)(this.b-32768),d=this.b-32768,a,b,e=this.c;if(s)c.set(e.subarray(32768,c.length));else{a=0;for(b=c.length;aa;++a)e[a]=e[d+a];this.b=32768;return e}; +u.prototype.A=function(c){var d,a=this.input.length/this.a+1|0,b,e,f,g=this.input,h=this.c;c&&("number"===typeof c.p&&(a=c.p),"number"===typeof c.v&&(a+=c.v));2>a?(b=(g.length-this.a)/this.o[2],f=258*(b/2)|0,e=fd&&(this.c.length=d),c=this.c);return this.buffer=c};function U(c,d){var a,b;this.input=c;this.a=0;if(d||!(d={}))d.index&&(this.a=d.index),d.verify&&(this.B=d.verify);a=c[this.a++];b=c[this.a++];switch(a&15){case V:this.method=V;break;default:throw Error("unsupported compression method");}if(0!==((a<<8)+b)%31)throw Error("invalid fcheck flag:"+((a<<8)+b)%31);if(b&32)throw Error("fdict flag is not supported");this.r=new u(c,{index:this.a,bufferSize:d.bufferSize,bufferType:d.bufferType,resize:d.resize})} +U.prototype.k=function(){var c=this.input,d,a;d=this.r.k();this.a=this.r.a;if(this.B){a=(c[this.a++]<<24|c[this.a++]<<16|c[this.a++]<<8|c[this.a++])>>>0;var b=d;if("string"===typeof b){var e=b.split(""),f,g;f=0;for(g=e.length;f>>0;b=e}for(var h=1,k=0,l=b.length,p,n=0;0>>0)throw Error("invalid adler-32 checksum");}return d};var V=8;q("Zlib.Inflate",U);q("Zlib.Inflate.prototype.decompress",U.prototype.k);var W={ADAPTIVE:x.t,BLOCK:x.u},X,Y,Z,$;if(Object.keys)X=Object.keys(W);else for(Y in X=[],Z=0,W)X[Z++]=Y;Z=0;for($=X.length;Z<$;++Z)Y=X[Z],q("Zlib.Inflate.BufferType."+Y,W[Y]);}).call(this); //@ sourceMappingURL=inflate.min.js.map diff --git a/sources/3rdparty/inflate.min.js.map b/sources/3rdparty/inflate.min.js.map new file mode 100644 index 0000000..42a5cec --- /dev/null +++ b/sources/3rdparty/inflate.min.js.map @@ -0,0 +1,8 @@ +{ +"version":3, +"file":"./inflate.min.js", +"lineCount":15, +"mappings":"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", +"sources":["../closure-primitives/base.js","../define/typedarray/hybrid.js","../src/huffman.js","../src/rawinflate.js","../src/inflate.js","../src/adler32.js","../src/util.js","../src/zlib.js","../export/inflate.js","../src/export_object.js"], +"names":["goog.global","goog.exportPath_","name","opt_object","parts","split","cur","execScript","part","length","shift","undefined","USE_TYPEDARRAY","Uint8Array","Uint16Array","Uint32Array","DataView","Zlib.Huffman.buildHuffmanTable","lengths","listSize","maxCodeLength","minCodeLength","Number","POSITIVE_INFINITY","size","table","bitLength","code","skip","reversed","rtemp","i","j","value","Array","Zlib.RawInflate","input","opt_params","blocks","bufferSize","ZLIB_RAW_INFLATE_BUFFER_SIZE","bitsbuflen","bitsbuf","ip","totalpos","bfinal","bufferType","Zlib.RawInflate.BufferType.ADAPTIVE","resize","Zlib.RawInflate.BufferType.BLOCK","op","Zlib.RawInflate.MaxBackwardLength","output","Zlib.RawInflate.MaxCopyLength","expandBuffer","expandBufferAdaptive","concatBuffer","concatBufferDynamic","decodeHuffman","decodeHuffmanAdaptive","Error","BLOCK","ADAPTIVE","Zlib.RawInflate.BufferType","prototype","decompress","Zlib.RawInflate.prototype.decompress","hdr","readBits","parseBlock","inputLength","len","nlen","olength","preCopy","set","subarray","Zlib.RawInflate.FixedLiteralLengthTable","Zlib.RawInflate.FixedDistanceTable","parseDynamicHuffmanBlock","Zlib.RawInflate.Order","Zlib.RawInflate.LengthCodeTable","Zlib.RawInflate.LengthExtraTable","Zlib.RawInflate.DistCodeTable","Zlib.RawInflate.DistExtraTable","il","Zlib.RawInflate.prototype.readBits","octet","Zlib.RawInflate.prototype.readCodeByTable","codeTable","codeWithLength","codeLength","Zlib.RawInflate.prototype.parseDynamicHuffmanBlock","decode","num","prev","repeat","readCodeByTable","hlit","hdist","hclen","codeLengths","Zlib.RawInflate.Order.length","codeLengthsTable","litlenLengths","distLengths","call","Zlib.RawInflate.prototype.decodeHuffman","litlen","dist","currentLitlenTable","ti","codeDist","Zlib.RawInflate.prototype.decodeHuffmanAdaptive","Zlib.RawInflate.prototype.expandBuffer","buffer","backward","push","Zlib.RawInflate.prototype.expandBufferAdaptive","opt_param","ratio","maxHuffCode","newSize","maxInflateSize","fixRatio","addRatio","Zlib.RawInflate.prototype.concatBuffer","pos","block","jl","slice","Zlib.RawInflate.prototype.concatBufferDynamic","Zlib.Inflate","cmf","flg","verify","Zlib.CompressionMethod.DEFLATE","method","rawinflate","Zlib.Inflate.prototype.decompress","adler32","array","tmp","charCodeAt","s1","s2","tlen","Zlib.Adler32.OptimizationParameter","DEFLATE","publicPath","keys","key","Object","exportKeyValue","object"] +} diff --git a/sources/COPYING b/sources/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/sources/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU 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. But first, please read +. diff --git a/sources/README.md b/sources/README.md new file mode 100644 index 0000000..61cff33 --- /dev/null +++ b/sources/README.md @@ -0,0 +1,92 @@ +#A web client for WeeChat [![Build Status](https://api.travis-ci.org/glowing-bear/glowing-bear.png)](https://travis-ci.org/glowing-bear/glowing-bear?branch=master) + +Glowing Bear is a web frontend for the [WeeChat](http://weechat.org) IRC client and strives to be a modern interface. It relies on WeeChat to do all the heavy lifting and then provides some nice features on top of that, like embedding images, videos, and other content. The best part, however, is that you can use it from any modern internet device -- whether it's a computer, tablet, or smart phone -- and all your stuff is there, whereever you are. You don't have to deal with the messy technical details, and all you need to have installed is a browser or our app. + +##Getting Started + + +Glowing Bear connects to the WeeChat instance you're already running (version 0.4.2 or later is required), and you need to be able to establish a connection to the WeeChat host from your device. It makes use of the relay plugin, and therefore you need to set up a relay. If you want to get started as quickly as possible, use these commands in WeeChat: + + /relay add weechat 9001 + /set relay.network.password YOURPASSWORD + +Now point your browser to the [Glowing Bear](http://www.glowing-bear.org)! If you're having trouble connecting, check that the host and port of your WeeChat host are entered correctly, and that your server's firewall permits incoming connections on the relay port. + +Please note that the above instructions set up an *unencrypted* relay, and all your data will be transmitted in clear. Therefore, we strongly recommend that you set up encryption if you want to keep using Glowing Bear. We've written [a detailed guide on how to set up a trusted secure relay](https://4z2.de/2014/07/06/weechat-trusted-relay) for you. + +You can run Glowing Bear in many ways: use it like any other webpage, as an app in Firefox (choose "Install app" on the landing page) or Chrome ("Tools", then "Create application shortcuts"), or a full-screen Chrome app on Android ("Add to homescreen"). We also provide an [Android app](https://play.google.com/store/apps/details?id=com.glowing_bear) that you can install from the Google Play Store, and a [Firefox OS app](https://marketplace.firefox.com/app/glowing-bear/) in the Firefox Marketplace. + +Android app on Google PlayFirefox OS app in the Firefox Marketplace + +##Screenshots + +Running as Chrome application in a separate window on Windows and as Android app: + +![Glowing bear screenshot](https://4z2.de/glowingbear.png) + +Are you good with design? We'd love your help! +![Glowing Bear screenshot with lots of Comic Sans MS](https://4z2.de/glowing-bear3.png) + +##How it Works + +What follows is a more technical explanation of how Glowing Bear works, and you don't need to understand it to use it. + +Glowing Bear uses WeeChat directly as its backend through the relay plugin. This means that we can connect to WeeChat directly from the browser using WebSockets. Therefore, the client does not need a special "backend service", and you don't have to install anything. A connection is made from your browser to your WeeChat, with no services in between. Thus, Glowing Bear is written purely in client-side JavaScript with a bit of HTML and CSS. + +##FAQ + +- *Can I use Glowing Bear to access a machine or port not exposed to the internet by passing the connection through my server?* No, that's not what Glowing Bear does. You can use a websocket proxy module for your webserver to forward `/weechat` to your WeeChat instance though. Here are some pointers you might find helpful for setting this up with [nginx](http://nginx.com/blog/websocket-nginx/) or [apache](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html). +- *How does the encryption work?* TLS is used for securing the connection if you enable encryption. This is handled by your browser, and we have no influence on certificate handling, etc. You can find more detailed instructions on how to communicate securely in the "encryption instructions" tab on the [landing page](http://www.glowing-bear.org). A detailed guide on setting up a trusted secure relay is available [here](https://4z2.de/2014/07/06/weechat-trusted-relay). + +##Development + +###Setup +Getting started with the development of Glowing Bear is really simple, partly because we don't have a build process (pure client-side JS, remember). All you have to do is clone the repository, fire up a webserver to host the files, and start fiddling around. You can try out your changes by reloading the page. + +Here's a simple example using the python simple web server: +```bash +git clone https://github.com/glowing-bear/glowing-bear +cd glowing-bear +# python 2.* +python -m SimpleHTTPServer +# or python 3.* +python -m http.server +``` + +Now you can point your browser to [http://localhost:8000](http://localhost:8000)! + +Remember that **you don't need to host Glowing Bear yourself to use it**, you can just use [our hosted version](http://www.glowing-bear.org) powered by GitHub pages, and we'll take care of updates for you. Your browser connects to WeeChat directly, so it does not matter where Glowing Bear is hosted. + +If you'd prefer a version hosted with HTTPS, GitHub serves that as well with an undocumented, not officially supported (by GitHub) link. Be careful though, it might break any minute. Anyway, here's the link: [secret GitHub HTTPS link](https://glowing-bear.github.io/glowing-bear/). + +You can also use the latest and greatest development version of Glowing Bear at [https://latest.glowing-bear.org/](https://latest.glowing-bear.org/). + +###Running the tests +Glowing Bear uses Karma and Jasmine to run its unit tests. To run the tests locally, you will first need to install `npm` on your machine. Check out the wonderful [nvm](https://github.com/creationix/nvm) if you don't know it already, it's highly recommended. + +Once this is done, you will need to retrieve the necessary packages for testing Glowing-Bear (first, you might want to use `npm link` on any packages you have already installed globally): + +`$ npm install` + +Finally, you can run the unit tests: + +`$ npm test` + +Or the end to end tests: +`$ npm run protractor` + +**Note**: the end to end tests assume that a web server is hosting Glowing Bear on `localhost:8000` and that a WeeChat relay is configured on port 9001. + +##Contributing + +Whether you are interested in contributing or simply want to talk about the project, join us at **#glowing-bear** on **freenode**! + +We appreciate all forms of contributions -- whether you're a coder, designer, or user, we are always curious what you have to say. Whether you have suggestions or already implemented a solution, let us know and we'll try to help. We're also very keen to hear which devices and platforms Glowing Bear works on (or doesn't), as we're a small team and don't have access to the resources we would need to test it everywhere. + +If you wish to submit code, we try to make the contribution process as simple as possible. Any pull request that is submitted has to go through automatic and manual testing. Please make sure that your changes pass the [Travis](https://travis-ci.org/glowing-bear/glowing-bear) tests before submitting a pull request. Here is how you can run the tests: + +`$ ./run_tests.sh` + + We'd also like to ask you to join our IRC channel, #glowing-bear on freenode, so we can discuss your ideas and changes. + +If you're curious about the projects we're using, here's a list: [AngularJS](https://angularjs.org/), [Bootstrap](http://getbootstrap.com/), [Underscore](http://underscorejs.org/), [favico.js](http://lab.ejci.net/favico.js/), Emoji provided free by [Emoji One](http://emojione.com/), and [zlib.js](https://github.com/imaya/zlib.js). Technology-wise, [WebSockets](http://en.wikipedia.org/wiki/WebSocket) are the most important part, but we also use [local storage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage), the [Notification Web API](https://developer.mozilla.org/en/docs/Web/API/notification), and last (but not least) [Apache Cordova](https://cordova.apache.org/) for our mobile app. diff --git a/sources/assets/audio/sonar.mp3 b/sources/assets/audio/sonar.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3cc376a0a1380d73fd8c517a87f15dba54b178e6 GIT binary patch literal 10368 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 VfNU%g_`fP1D|KCpw*HTS{9n1TVbcHr literal 0 HcmV?d00001 diff --git a/sources/assets/audio/sonar.ogg b/sources/assets/audio/sonar.ogg new file mode 100644 index 0000000000000000000000000000000000000000..77aadec27c55e47b116c47a80ae6652bebd0afc5 GIT binary patch literal 20011 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 S(-UuRZ^eCjdg8ym72FD?EI=Os literal 0 HcmV?d00001 diff --git a/sources/assets/img/badge_firefoxos.png b/sources/assets/img/badge_firefoxos.png new file mode 100644 index 0000000000000000000000000000000000000000..3bb6991c3490120b0e92a60dfc566828c2d17df8 GIT binary patch literal 4293 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 literal 0 HcmV?d00001 diff --git a/sources/assets/img/badge_playstore.png b/sources/assets/img/badge_playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..9891cbbd39d36e215940fa18a1fd1a0a83bcb9af GIT binary patch literal 8913 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 ZKLJR}EzL1fdgcHC002ovPDHLkV1i4h+i?H@ literal 0 HcmV?d00001 diff --git a/sources/assets/img/glowing-bear.png b/sources/assets/img/glowing-bear.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf2ca2b3a1a5b51a35f35ceefddca8713599d8f GIT binary patch literal 57024 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 with Sketch (http://www.bohemiancoding.com/sketch) \ No newline at end of file diff --git a/sources/assets/img/glowing_bear_128x128.png b/sources/assets/img/glowing_bear_128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..2fec46993723dec89efda3e29318e3120cc402ed GIT binary patch literal 12309 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

    O z?0GU(%=t9?2JR$jL`h$QDpe2o{9bqUUi|zv>3nymD(XJqUC>ntLx02s@T96I2>g`&Ae4By;G-Pu4=cqS z@$dYdYx3D)ntFggXlDF@C&y0_%1v9`|&v8( z@w-tY?HCut5TjTjIjw>cfuCnSm&8V2l-RLv|E(64uryC`Q4)OmQ;Y#vI}i#2iFM_W zi`M^{6Rh?POx`E~e#RDjqM}tlfQQ-eB-KnES95K;n+CY36o+-5W{(V(ruk^0o>whaKEJfIbKXhDS)1miLNvN&&aXfjFkH`a(o~oGlP(9MowlMgfe8l zHd0Ou?!kNZ;Jtfgx^EAZ2QqXwS~4_-h~T=0ur+wcyKHpC6Tehk7@rCErNHAelLQQ8 z2Iv`rX@EV}*C^$ICOLsV&kQ!MjcbTvzUS}o8s5`{agq2k)k89nL?i;-B0dMiUY`Wm z-X#W1)Bow0lmu9@y~N)kcc2rQC4ClOVr^b0_V&sWF&7uOOClW9J^&9V zqqGj~-?o3%D$M$P1^-~g_<1lnj@gmGppuxuNty0RNx3gAGXp6oA}O=EK<4v-EEEG- z98N$2ihe&ooR;}wO6Cd~Ih}8iQ$tWF2|+T@IviE20QXQN5t0nJ=oZu(_+5e{zFA-E z&U%oUKA!OktypJrX!{7bhum6I&ZMQmzSew_0y}h?a z%;Cz$=YQf8WSC|K5rHxybpk#ps~$5yBKboxf=BnBCm z*`YWikx8UQMj9X-^oiiRl8Q>ex zhYX0BmbttEJU}1g=<9o?&luYX$AGbLOc)!-Xnw|+@%jXG&<Blcz$>f-)+%^ za_j1%+`D&A9# z%R2_I1fM1!;|c7nPZ5}7y#Rh7O~B?00DU+n=co6{)w4zP@sd1jgD#+xC0RMq3L+$E z9gou*WE>I%hKTHwu6UFL0hV}n!w$QKf6TzpB#U(4VDHY?#XT#aSMazM!n1B5sBwT+ z0^`63KS0l^eF4?LM+iP=zg1X0d?djCFb44Ba&vn_(;_57o-qUC1hoc;4M2jlk^tnakifA9ju3oC`5y3-AamLP0$?%(2tXO&(T79m z%Q<;W4LZ62f**jTG!rM~kN`0ONkA(=om*ExEP#pb8uIIJ{PH%9<2aea0U$AV+S?Gp zeKu|!z_lYD!V)m|tXl#pNt%An{#wj_1AYVnUsOsh0KWs^cLV(Xh|CYw$(8wjviwQ3 z+t<#>`SL;f+fxX#U=0|Ijl+*JKn#&!fXXvjH_jr8v&3`3$KNIp-KPM43V~V%vpxl2 zj2nu|h4TCO>?L^u4Z6B3mlv`mf%(iSx&yP?2)Io2xdOEUD*yU7e`K)ZX(0zwVr*U~ z=1%*9UTPFN51^4h&+pUs<3x*H^E&6ziv>T$z;!Mc!>o_UwKF+>=96w$ z&W_4#HbDH8LJ)11nn`s9Y6WP$G>!o_#0Y0K6*pcg;hKNFbc>if?HRq+EPT098ik6q z0yq<3Y65ru^Iwtx>Nny08Tb{NqX_zt7l|sY!4&r<0L^N zW)4Zf#ie%zr=4KW0D7e-Y+?tSu(fnEfaF>Sd`Ik$ZP<$y08KuvbWeQ?LXiK@YQU%p&UQ zfO`r6+Yo`m96qypHrlU%4?i4W<`aBI?@zfwhqq;+07CaCUOlGt?v#OBYw z@@obw5@27=4sLSk?00Tj+rHifhnGV40dSM#U8E85P{ahUyXKz+$kO~rqy+PSv?C_v zo_aYw@Qz%b>yb@rB*Efn6PAh@m`{^QbeD*92OxnZ0t8=mvxW&C zqX~k)S?$u?0GH4pB$m}lCf2wtJU}<#Cc1Dg@O1|7ah&JSEpGR|0Aft6{q0}$x--4; z%5IKXY2ymI8rD_@Ai)qV0fpe_ znB>uj9|@~oBx#`Z#X^nTym(SJv)#V2ie+>heM-YJC266U96C@*3~6427@-uL8Cu28 zU}-QV0f_PX?PIz*6#dR?N8I9ww#M&jx>T2c4gn%0z+_hh=D$uBayuC$ZFakOx(D%q znaRw}%NSh&;sKH%4H6^)Jo5##3?+b%8HT7k#oWbwu3j$9cE~0->T)iJ*s3o;Dm{sQ z23T>$83zn74yafFVstQg)2)3Vmb&>vj5lb1qIci5HvUIh380%LZt8acso$Xk^#6#M zqsaJ0vGl8vvm6^ZReO+?ILffz*0J$!HjR~|%wEC~=1 z5}+$8r?w0M)PMFNKagaeotZ)t3$!(m04jDz53i-@`)B$R$Y0bTj_DM(Ekl3=2{2kE z0StD|U5YVcTO|RWg_L;a{L3%)C;`+3_$>elKnt*#dyCQQrZ;BRR*8@UzZZV8wA1vFb0TX_SDG2&|A!sZIQ-ZMQk}UFTgZ$N2AaRv;us{Fl3aQJSn8F zQ&@M`%bDRi*&+?PJd4#x6fuBtQ49h#24EJ}M}VeXI~es7Ltsf_MQ60`uz6AiKz+SSD@{D0KR1lj=bkotd109)kZ6uuHWOM*|k1gJI;{B2(R_}R}C z!b?GaE+7ii6orY33HJiBumXjha%T&~0t>@QY!D^w)+%!Wb}0y9)h_qkXb^gxS1|;G8gk;*J`tah+sPio#`}O#)4W0BXT*T|O>h~g#i0_w(O0s^)e&qcLQLcvm;0=5W`F|jE!PB zRV$16U9vKr*)$SdINmIrLA@r(W3H37;HtN?Z#cT>VgHn0M$2tq6{NkvAD zKP3rPryDV_Yw{Eu>+$At_1w{$>Sjq7XL`Yc&s4D81Y+e+Ye4dryJJXb)xrvV56_r; z%HUs@3&-BW4j|I%*}xJvC0zUK?Ek-Y)s!IT8HE_Qb zFeEq>St|_bN`M5h3JK1SN96XES$PVL^@B^L1ZIB>*2pZMj1qYYiL+j}g?Y-z9#|=| z0w`|DTZh%aZaG(~W1ZjAsb`pzKbpcbSDxrn@9I)Nl20`(JVDLj8u%SN zg8-d+OTpHg{lv^*r^!5G`Vj4>?VWzd-`!;|8}wKK-5c-~hZdS+Mwylb0TL`Rrda25 zh_H*hq11@nSk6E8l<|#~f5i_|4>nCdh74`Fv^2n0XycBAYUOt0>PE35@v}wXXZk#7L4B|7bfDg2zPHR|7iaH z(kY?u%-6!&aOT7Bo5(%@_%!iaR_&fHrs&~S^cnAAWiz+8l>{?=wG0Xu^KZ%OvAy`L z8TnBf#)>ibA<9o-xmnLye>$6hZp`5sbG%H=ep10#zwa^q-q}w9cEpebAg0x~FR~2$ zx#x1hx4*6rr1)0^?!i#H0*3^qSTv3SCE$n6ArWH@;w%>j5eV2)@oo58vs!2%8E##i zhTpxok=X=z6Xn;vQs;gh@bmaH7v- zIANlPcFR)Xd-%*WKD(1-*vOq>EErSSfQ~UX!J5=y%mBVX;4$lyob_7$i&zCZOZdR| zn11if7(~u*m;^b`XVy_YXWNgHg`<=^nF`og&3avdp9FeJ*eEn&PYK6t#ckZKxs&wKh|*c@a6 zd0dkP2x18QK4Po~fQa&M6nMlMH2EVSS*b ztvqIbBfswufS3V0Vn_lI)9Tw-Ao9r{`*^=|vZPuPcn?{sOTgd=42i(x3(pT)GH^eh z6*;gD%ztAIPb+bP-^ckCuW{E7@8#c2Sl~z70s@btX(ZbaXFb8EnfKf+MA7dZB7g)u z@4y2ud0GP-&$RK>6JwgGZpJlqIUu3w8~VzxRsxQp+{c-Y{-$-d8&l62PvBb^pt`f3 zI@3OTz^8<-z42W1NeoE;7Umq%#@UUpkXF7%8Rl%K zsSg8hXFZ9h=~B;aA^yu9U26a^tTnK)fhBLDLI&mz*jMz8eRTS3`x+R)Y2u3v?&1X9 ziA@~fy|do+`5S;wA!fji7?J?QwEDJ<*N;5&|EA4DG!CeO1QhR$8zB=*#qum3g)>ZD z1n&@{kQlBg*s4H)uBJZX1zzKk+=po?b-gE}z3fr zjCJpq;m&uNC+KnV8!izTAK2iQ{Uw1(Vt)1aH{tY`pes>n>RsSnvOX&K>i;3ckOUy6 z)wiFt|G{EbAEZ}|R7-;Ot-(i#;2{Hr3Q|&J9T`&P0RYSz1zpW1MW0=0K@n)*2Qppg zkYmM80N&w9dq|wuiX9+EtBfAmC)wttS}>m#gVbw(Y0*4=RntxxaQf@H7S1>8HwyT| zLaZ!q#E=9arq#Dk+CN%x{(*B9H*9)I;3tCb+BHVuB!n)>sI0K)>v*r{qQ!Rrd=mjI z8_@r+y(>O8|WGfw)ao#{+7ZGSjve`NBjGwoOtNfXg5%2GjvM1zP#%mQ8&1!M`3 zfDjbLfT%zeWf3A=ARx=VAXo6pa@k!(V<*$;OxwwHnNFAftL@wS9KN?1&)nsl!*}mR zz3ZY(F_ zw)ar$Ocp~;9)L0Z?XeyRq*r)-1wmC9ZVa^;%CA~(xfyjpaZ!p~0((6qgm`r4c5;`u5%TXm97SCmm@`EjZ_ zT|sr{&QQna7szq-GTk1x(*5Z{JnOMA+!^`8DJb%y30^LUFfP10#4iKdc{vd3{fyu< z-}~+uzYsWEoPv_(zw1_1gfT$;U5CY`;0%a3H_}Hfjc2K}q=@#K_R!X?TWQm#O|)_2 zMv91t0PwT&f3dN#w0n0F9WKbHb2XJTZtHUPDNqn>E(-&2bJ(mYzYIB~DKwo?XP1HI;J7_4h)NRW-lzj2txBkR0<-Py{5BmGMHO8FnlE*gO z;S}un(N8EQCPpo+p@NW|nL*ahW_T2r>D`~UVRYaozkqMAzk?Rs_A{UvhlCee8Zyg5 z#K~2o{9^qh0;z=qOSm4zAM9>%*7_`pii(1O(s1BFDmB%gC2kVbxHn;?VRMCSk#+a{ z8PE*c2@MTf241q_N8h)r6F8NZ#1|3y6?fqJ-H1Om+(X4hg%lYX3DKk@B{`XT+b_CJ ze|q4e-}!(B&_b;bG=p|RL&Nq9F(v$4bppId#2rF^0B+V(?Z0@wn%%z7P*PNY5rxb2 zF$rZiSNM$&aLm8c0 z_qxr)tnG8;HDInEfL1(}FEli6e6MiG&z>7nC#1fb)Q7+{sz>pG+?+!`6+bL2jJ9mq zLX*SYNY71Qpx-azgL8L!Kr8kAz>Bdcu{3<&U4qpG@AEo=&oFbP$!h^9$j_w>8#ef? z0EC8yQbIz!M-KpC9l)1dfNOB>P7i1W%@pk<7={pk#tYNliTKh@q6j00;Yh&TKXAI@ zIKBGnt3DBb{rdH^YSk+GFej7d9oz@-douuz!L?iQp^*?Dnn61$R*9EB_v0VBZ3ck) z(o~;x3Ne4~O?=>LubDP)-t3b?5E2qXYuB!&+Nug~`9NXyS*H&id(r?JK`Wt|G+VVW z8u->9E3y(Ec*+1DB#{W5+|g73P&11P@@e(z)u^#jT|a97yYD7psOdEy7!6!ApOno5 zxb-MLw1Gy@N@yn4?&8JKJSpS>mriC%{qU*&1`w+W*DP(cb7um*_~MIF`U9x-5I;OT zjG8Z0^K76;+?}wZ(<+q%aO#bBXd}djR?=-le! zlRuz=u4Y|nq60W}wUtJD&Kvz+a0SjZiw`Y?_|OI#`P4FaV(3J6!oM|U5(^fFb`t@k zmPWcW-UotpwpIn#G3(&_aC?gipNy#}E4D&!6`=FW{zgsDHw!tCnW3eW6kJLuSv!!&!n*HBus*V>f=(A!w1cY=pId%6G(g!s_pX(#+*VSM{Uf$6QU^=2B= zm_46i|EbRSk!tJ9vwx+L*(aXQ+HdVo7FyTy?^8r z=&aSeJ_0UyYF8J)J>T*oH1VsotM&})1$wxe))2btN!r4T0n;UqTX9&pG%y1iV0k~XSq-9Q@d-;aId;WwLezz68Wh_2w&7{$f!1X5W zXL3Cb7cZU8V`8~4u;jpPKVa`|AZx=>Ps2NWGrrxh_~4wkdaB zJd5}1;Tzc1O940*;)8Q=za%Wwi3d*nCvnr-X=GF;5bdhEcpr5&oP3DvTcbSz760Z) zH|K>x)t{tOv$Fso&$4*{x8PWC4bGR8<%*K|xAdgQ`?5d&Xxes4iQ7O^gO`|W_5=_k z;58o@Y^$MtJHn|VCsDfi;1t|)^zWr6K5rm3i+uxM=cmN{PBsat3E@jy{Rirv1PnjiQ+fsKcVvgQfU*}{+u0-*OjVrhafenEHBNu;abP*Rla zaXt_AY=XPT!k*?KA|^5rx1b8c$%8hEEXSLNJ_g8F2D)6 z;h~=3?6I<^#016zUOQOq0upfi&<;AVGxD1n`Cbh5x7A1$e)6iBT0T3X6f#aH>)mK7 zmoVW24tQ=~aFZ*zdTcEe^VqzKxR|A625KQ(F@#~{r^b`D^$bngj0->B-%K5K$0;>& zBfpnYocL%@tQ41zZwCiFw=cMPBJ3GnMdVE-GQ7dZrZ;Kt_AnNUK}8u!)YVu{WBpAu zVKYCpsKl7IwJ6if_TGAGt}2EnQNKs=G$jY)q||!e%puS(IC-LMnOMvI7jG}a{x;rd zY80P3y+K~`7BaEBc7y#pBB}5|0_B=Dec|bn_ZX6>Awb-cTm}W-j4z>ML3B7yd9pAu*HUz#B-x@SGTh251@Clzq z#Gyykuu;lnQd#B>>5Df!tV5ppteybADP8;UE?5vud=`<9rsM282dP+!K;Do?KB6sv zZ!n2(3Yvifsk``SIs7|GU*2jnFxSla_Ly^KJJhn%qRoPrzt75C?$V}_fwaGe_N)C2G{{tv&y-{5aO zya#DmkS5YzdegQ+@32yYM=ul?CEVcaZ*&6B+z{_56z^OatmTjHtRpT>dRAPY79uWE vj1j=kA>wz>;%^`G*}0$vEoeatTG0Ln4o_2nxYRT+00000NkvXXu0mjfyhJ%q literal 0 HcmV?d00001 diff --git a/sources/assets/img/glowing_bear_60x60.png b/sources/assets/img/glowing_bear_60x60.png new file mode 100644 index 0000000000000000000000000000000000000000..9dbfffd77ce3c6dbefff2219a5662580d9a5736c GIT binary patch literal 4883 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 FV1oOCf?fas literal 0 HcmV?d00001 diff --git a/sources/assets/img/glowing_bear_90x90.png b/sources/assets/img/glowing_bear_90x90.png new file mode 100644 index 0000000000000000000000000000000000000000..11cce25dac5e6c358f034d2e7243b6b324df2730 GIT binary patch literal 9713 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

    TxQyz*Y~ zI}P^+x)Yum4R<5hOm4<<@o&#^;qPV4X9$y4w8g#K)^{gbkzekqV#?Gqhr-x{j==hr zGt!U_cB<7Il$%@`VbY!gGu6q6hj|Rdz{&yR*1&shG@Ud+Ha%_8`94B@JNy7GEN!SsL zzk~QUzr!_wxHdp{hHjv4++nj>@npvdcQfG{?nbBU4yQ~cQp-e`WFq)CL74!LGU0W( z%%3z^DPLh^CFdb9qb3{Nv~5&N@iG}{;+3%5B>LpefV3h{6re`iNwkyeWL(0b&`jEo zMLwAtTfR&QhSR(m2}in9MMLslpY|k!1iKnT0SzJ(b_Pr0u24C#TjP_kyVxBjNxMQM z5#V*ao*kkaXK1=-4R`kv+%=f;+$)nNHrORo(r&;_rX+x$QKrP5Bwx$qMtD5Xvwag$ z;rG*}5Uwk3lsD4lt-Ac&rR^m8)Xo4Wy)k}HqOH}jkjgc_e{1wOm7!xkxg7EULC7Eb zA^vCXf5O^7vUtE z?S%&E5M61_Jhoj|UT(o?8FiOtHM6!`+poK&ztx9lf++k1JZJy}Vu;!dV6QMMi_u##F zPu`n#pspxY=Vz#!GPq=l$LrxJ$P@*cqaZuUx1cRyF2dPHwpDc^-BG48KWC~~7*haL~MK7-DpT6W#J>i*1X3s+lEb@6=gj_vp zQNn+7!cTtubd~&eBS!ArIw2`Po|EJs&dTlYj>)grcgqjw!{yskf%5fD#T@!dgLy9!d>02CMvi(R z9EQ#Q)9FVelz-Q%7jSpFlKnGB_ccd`Sfp&=TB73X@>%b*0uE_vjwU%&J_X5_k654; zp>pe7wA}tCUL3YNl2Ngz{i2Bf>Eass?z9DjThLL^zPLy^Pwn2TO^NnW+}tmcPk>}) zDmW7O8~#?A0TRqk{Ps+Q{CIIKIy^zunL(lKXu}b-1xk|;g0_XBjWmnYY&B83CdpZ2 z2Oyq!STva`3B|~eiKTe>-$Qz|w(@&?Hxf`Qx6<;>I;jjDxrRpCCYAxkZh2u zePHDl8umCuc`}?_&&Nh8dwUJ{;JyBJWv8V6>k{k8yjZqm`R!(`e3cX|SB`{|Y_^$g z*K>%U>|3=&xI#~kxIW5>FceA(r-FM}?Dgxsn9lEjaz^%Ue~!l1Le*S=O~&ycp(>74CAx%-Kjv_(;{Dx z?AW_xA1B#{4-WfHD3T$I(qvo<^8*`f!3YV%z0TvFsq)u~6QeE(njIuxpsnX)LSfcI zTw0w3E+!7^t~h$R+OtSi3{YDV$RQ)`;zbG>D=n@v0+6xqbh%@!<*{dckwmFkt-S0A zo5!JI)@IDmVX$K%^2@at$m5W#D(T7J$*t2QI}D98%!oyMB(&QKnq<0?7G6V52FVff z-5HCd-~LLnvi$tTR=IvONG=|vR)?6hdL<)c72M57j)R=h^pfbw@AfeA(SyxIR|ak% z#I$1nhK(yMyDf8-j~IzfFUNJXl&~XEPQo^y+pn}46HK_I-?=4OU2G{o%B{0ua{X8s zY<7g6AVKyg;gSF|LEt?+iz0iS0TAGa?fI`4M#UGF!?HVU1Vn-5Ps11J8_#MQV~;*c+x#Uw4$s?r;mY;W`_-(w zBNo_Il13A4_|B|^-i9I?L}X{N{&royJ{gK&LuoETXObO7K${W89c0iGC10P4khI^w zmTWBWvt#9Yyq|ogh(_9AN3bT^h+KW)vH934lq;3SkQ-c%@<_6;udlHzZULkhc6u6# zdiL5;lSW@qM2E=_7*5$;zJsm43j53O_$)N|^nQZxqwyJ|y$Jc?vrX`SQzRP;eDU>i zEk2N=*{snnqGQ(jI6JkdZ`H8}6aAix)9HLjL>|hQFJD>w`CYlEN~HXoXlk_CMu%d^ zD*su&MWA6%{aLcJ{B&sp{NhjsA#mZMp~(tzE>`7C5b8(3E6CoFMK|%Aq?MSLLX=y3 zQrl>=>DU=Q&rz+?Gsz6v(G($3 z@O~ZAwCU)9*9^-9K)65Iwf45rWIlE(^8x==C0*pp<3X4dw==TK`bJuRmTyi5!+i_a zE*>XCB8u<%874#KGonGrPm4GY~Q+Q%EB zqw?g=CDFc993+~ct)}^)Kh2&@)LMN(C6ppyLV_^Fx6qbltxI(y7L5#69jYQkxOXS^ z0DB|o>hUub(0q9;SbqN|F^gGFN3LWGavg!_%y4>IY_ky+y3)7Q=0hCyfZs=>(%>G` ze~$Bp=yW45E2rBT*N% ziNOlNytRcnXWiN1h};4s?M~K2g`D7j`qY9TD1@nBm<$F-%(-~hX2lVJout;n1@qB| z{?;3!Z7lUDk!!ZGId5Pu>OmOIA4Uy*NJ|T`pXr%2@j~mw5*$*4muxzTL0?c~6$%`T$XEuU&DR=6OEPWOgr>$9bcSvUHBU^lNa>q(In z=!sFS6H)*+miCevH>NqE3I6aq*T|jkjw2WH7065Xg5Y#X`|XqLYNz@pG;rlJ4tP*A$T+QiP1&&PNmr#+8D;P znOw?>!qO0i-U%)SM9R;X*2r(ycS2i|K)~mc{`*Zd?xv*udIj2liYp#8JMhax)x>iJ zgk8Lcae0N8wgFmeM8b9L(2Dw^4piY z;HR&a@6W6JHDnHl?2&8()2?ijiDZ{uzH2fF)3%2#{?pR19mAoCE}9|HFH|h!uq+tm z)I@vOJK;t)&B$(srX0!gy07=MRy zd5f|^MtU#w%Nf>l!I&%bVa8%H>+wV~IWcYSSt1*kPjcW{7pEq=bVfvT51C`-jKPX% zZ^+gS)=T?(-EKcU1-f9a)o?UMX*#nfo>tLRBgNV7RLmY5^UQ;dQxr>jQtwg;d@P_tbUkdd+oH7RtmC zc-mFB-eaP7EpgNs`#It~mFDJ1$a}BWE&IY zhP(UQT!b4Mt~anG0rU_!ah)Re@|ECc+%?2&+z5uYQj<>ZiR|S4Ys7X$3 zHdMbkcW9GbIek#BoH-lglS#VoSJd#XsFUP#m-P7^(u)|yT?kFzi4jc#fmF?TO_<33BjK8K zy<1ZwKsU;On%Jnp)JBlCjs%iC7s13AAXed|0TbB-I_2uwIQjg)WQ%ej1@~O2n{cL57*GZ-E1KQY0H20D5G292=X}`<_O}+{IoLWasG@A!Q zdr2m7{J=KZxGF@#0+vdM-^a2hB2W(R-K?&;itqXAPWq*UJ%)|_W4#+aKY2i8_Q?Zo zYUCE)ejOU5WB(nA{@5F$8w1j__3O7uG_lEVTIL>Y@Vx8O9sG`31(Nrf$tCdF0XepR zv-m8WBi*`plg5pkC^Tu(RH1Q`Ceo-;W9iVLqs;zrs>JMEi^-9lrAgO>e|mhUVQbyt zZW=QEObxH&!2#rm8y#D}S<^UDqKh{~KMUCFR;t0G6#ew#%_aIIpn$@ zaVB=XTiu@J(#qm(8#&VXcruCR)3c7+FH}(ZXm8K$Ce;oOP}wf z>!t`X0~$GI)Cdr$NOULmzJ#+w-Wk|m;`eQaxzU}c!8stSpLsO2nfPc_H%FP0MeOQ8 z7B9==Ksdl3L#q|p)NJlF&9Y~@Ih-%S4>ptW<@q>SGJlSt!4Hh7lg7!#r2TjA%~5h} zXO_rq4p7LA=_y)=-VhoAcwOED_*_P^`O!BH6Mc}$U&e{OQOZLw6MY3es z=aW~z08ay%DyOj#-8o@UyHxX|UIaHMh3Cn%3ZTt62l1N!xrS6-=2 zvNKCEG`gvB@vi*I{ZAiQ3!)ciMrKBTY%2p3vt6b()2*}NxS({sPFNmzpb`M^1e!EY zryHDUb%Rhc1~ zOH}S8yR^A&!&0;~oexD*ED=~Bh4IHbzyPPFdTOp(pW=uF%elj-mb686R$Z%Caw zb!d0*YqRUst0ym3uOT5Tmzex#&q&(8#%qmcn-~>qbKwr!=5!o8XVXMi1pZR_7~nZ~ zsUEc29ME`W`>0JPV%KFzG^4vvKOboTO|A9v%hIr6LlRC6cY#r3?b@}aE;_?!!L0k< zk6=FK_?}3wa(`U_?EQdeY!0etE+I#>bX}#>?gvIr3{&07=BZqpLKn!X zLmQD<+f5R@#>K>a61QiQ9N4i|&L49NcrwuDbMf0H5gy0ZFv|2tg87gSSCsBYus)lE z;kFS4o>PmkSzLMnQo3Z}bewHx%ZO-0;I+OJl{qn=rWo!sSxz3@0J1kRMVcA1>9%pg z%VY=bfi`cD1Wbg{%P`tZxn?Q@&yb?L8Ia70p9c-v7%}Sw*nU8^6y@$UtW(uUHf03b zISxf0n#$A1xEYWeC}rQKAT^ZF(x)aN*#l01=+Moou3c+Z@K_?lFXl1JJX^zMi@i`D z$LlXWm+G1Tv#}He5&-GnvB4dE0H3!)(ic@GCE91a1TLHiJH1JgV%CA|?Z#x5m7vK$ z9a~f#tjl-y`?40^5oIv@#a@C(g}wfFA(+xB+caw2zr-j2+lcwy*M~ zrjdwj(QHn*yf^2AX#d$b`K+aJK2)Z_(K}^G2dg$0sX$raKT-y=Qk;5V!-*@_2NC{* z_d3a(2?ON#zSSf{;mqN!u%~+sx9@@RS;8LLf8JAd>NdZqymoP(DRo1K~&&0Y$O>DO8jvD$x*B*iL%%%JZ}O($s}ehW1g*vj|# zY=i52_fFy~NgmL#0sY=glRW92cGes@9!+zR{5L5R0UHAvWco<9lfrt_ARZ<3e(x60 z?55&3f1DiI6-iP_ln1Oe!{aslZOoPsSuk~o4D8%c(D^)9)Z0mqo$u(`x*m_Zv}v0> z3iw-9ld)G>&ACf=58jp8RfCSsgHkgc%SGNb`C%k}*-*p)pQx?Dw=`9qEmzNpcW|{=iOe%Jh*vWZCRd zV)6YzqE^mS2>W=N_{fOsp}|D7QilHrO9dbV_yTb1^~lst&FZA!KNH!p z`P~jJwo~&sK{1a)JGzYUyqv`Ecnz=RZxAvotyTAe*4sNbtD91;RMAuuxtD<;CCmST z@-)y(PdvXE2N~kgF$ey?N&1_$t6ICZc-8uD_hwdj1F2jDT{^iR_m^04D6IplJPy9+ zXY*idaZMVoBRqmabnHVf&VP$#G?yB@I61{ce!)s;I-;ECe8g82n3n{MRZ;N6t@Yq9WWQzu6^2H z0b^CesbSZj{i4mm4*i?9ir9b3Kjoj2jphFbFOcOL)!ev(00000NkvXXu0mjf{~bkhN_!|Wn*Vos8{TEhUT@5e;_WJsIMMcG5%>DiS&dv_N`4@J0cnAQ-#>RjZ z00W5t&tJ^l-QC*ST1-p~00u^9XJ=AUl7oW-;2a+x2k__T=grN{+1c4XK0ZL~^z^i$ zp&>vEhr@4fZWb380S18T&!0cQ3IKpHF)?v=b_NIm0Q>vwY7D0baZ)n z31Fa5sELUQARIVaU0nqf0XzT+fB_63aA;@<$l~wse|mcA;^G1TmX?-)e)jkGPfkuA z92@|!<>h5S_4f8QP-JRq>d&7)^Yin8l7K8gED$&_FaV?gY+wLjpoW%~7NDe=nHfMG z5DO3j{R9kv5GbssrUpO)OyvVrlx>u0UKD0i;Dpm5S5dY16(DL5l{ixz|mhJU@&-OWCTb7_%}8-fE(P~+XIRO zJU|wp1|S>|J3KrLcz^+v1f&BDpd>&MAaibR4#5A_4(MucZwG9E1h4@u0P@C8;oo+g zIVj7kfJi{oV~E(NZ*h(@^-(Q(C`Psb3KZ{N;^GB(a8NE*Vwc715!9 zr-H4Ao|T_c6+VT_JH9H+P3>iXSt!a$F`>s`jn`w9GZ_~B!{0soaiV|O_c^R2aWa%}O3jUE)WO=pa zs~_Wz08z|ieY5A%$@FcBF9^!1a}m5ks@7gjn;67N>}S~Hrm`4sM5Hh`q7&5-N{|31 z6x1{ol7BnskoViZ0GqbLa#kW`Z)VCjt1MysKg|rT zi!?s##Ck>8c zpi|>$lGlw#@yMNi&V4`6OBGJ(H&7lqLlcTQ&1zWriG_fL>BnFcr~?;E93{M-xIozQ zO=EHQ#+?<}%@wbWWv23#!V70h9MOuUVaU>3kpTvYfc|LBw?&b*89~Gc9i&8tlT#kF ztpbZoAzkdB+UTy=tx%L3Z4)I{zY(Kb)eg{InobSJmNwPZt$14aS-uc4eKuY8h$dtfyxu^a%zA)>fYI&)@ZXky?^{5>xSC?;w4r&td6vBdi%vHm4=XJH!3yL3?Ep+T5aU_>i;yr_XGq zxZfCzUU@GvnoIk+_Nd`aky>S&H!b*{A%L>?*XPAgWL(Vf(k7qUS}>Zn=U(ZfcOc{B z3*tOHH@t5Ub5D~#N7!Fxx}P2)sy{vE_l(R7$aW&CX>c|&HY+7};vUIietK%}!phrCuh+;C@1usp;XLU<8Gq8P!rEI3ieg#W$!= zQcZr{hp>8sF?k&Yl0?B84OneiQxef-4TEFrq3O~JAZR}yEJHA|Xkqd49tR&8oq{zP zY@>J^HBV*(gJvJZc_0VFN7Sx?H7#75E3#?N8Z!C+_f53YU}pyggxx1?wQi5Yb-_`I`_V*SMx5+*P^b=ec5RON-k1cIlsBLk}(HiaJyab0`CI zo0{=1_LO$~oE2%Tl_}KURuX<`+mQN_sTdM&* zkFf!Xtl^e^gTy6ON=&gTn6)$JHQq2)33R@_!#9?BLNq-Wi{U|rVX7Vny$l6#+SZ@KvQt@VYb%<9JfapI^b9j=wa+Tqb4ei;8c5 z&1>Uz@lVFv6T4Z*YU$r4G`g=91lSeA<=GRZ!*KTWKDPR}NPUW%peCUj`Ix_LDq!8| zMH-V`Pv!a~QkTL||L@cqiTz)*G-0=ytr1KqTuFPan9y4gYD5>PleK`NZB$ev@W%t= zkp)_=lBUTLZJpAtZg;pjI;7r2y|26-N7&a(hX|`1YNM9N8{>8JAuv}hp1v`3JHT-=5lbXpbMq7X~2J5Kl zh7tyU`_AusMFZ{ej9D;Uyy;SQ!4nwgSnngsYBwdS&EO3NS*o04)*juAYl;57c2Ly0(DEZ8IY?zSph-kyxu+D`tt@oU{32J#I{vmy=#0ySPK zA+i(A3yl)qmTz*$dZi#y9FS;$;h%bY+;StNx{_R56Otq+?pGe^T^{5d7Gs&?`_r`8 zD&dzOA|j8@3A&FR5U3*eQNBf<4^4W_iS_()*8b4aaUzfk2 zzIcMWSEjm;EPZPk{j{1>oXd}pXAj!NaRm8{Sjz!D=~q3WJ@vmt6ND_?HI~|wUS1j5 z9!S1MKr7%nxoJ3k`GB^7yV~*{n~O~n6($~x5Bu{7s|JyXbAyKI4+tO(zZYMslK;Zc zzeHGVl{`iP@jfSKq>R;{+djJ9n%$%EL()Uw+sykjNQdflkJZSjqV_QDWivbZS~S{K zkE@T^Jcv)Dfm93!mf$XYnCT--_A$zo9MOkPB6&diM8MwOfV?+ApNv`moV@nqn>&lv zYbN1-M|jc~sG|yLN^1R2=`+1ih3jCshg`iP&mY$GMTcY^W^T`WOCX!{-KHmZ#GiRH zYl{|+KLn5!PCLtBy~9i}`#d^gCDDx$+GQb~uc;V#K3OgbbOG0j5{BRG-si%Bo{@lB zGIt+Ain8^C`!*S0d0OSWVO+Z89}}O8aFTZ>p&k}2gGCV zh#<$gswePFxWGT$4DC^8@84_e*^KT74?7n8!$8cg=sL$OlKr&HMh@Rr5%*Wr!xoOl zo7jItnj-xYgVTX)H1=A2bD(tleEH57#V{xAeW_ezISg5OC zg=k>hOLA^urTH_e6*vSYRqCm$J{xo}-x3@HH;bsHD1Z`Pzvsn}%cvfw%Q(}h`Dgtb z0_J^niUmoCM5$*f)6}}qi(u;cPgxfyeVaaVmOsG<)5`6tzU4wyhF;k|~|x>7-2hXpVBpc5k{L4M`Wbe6Q?tr^*B z`Y*>6*&R#~%JlBIitlZ^qGe3s21~h3U|&k%%jeMM;6!~UH|+0+<5V-_zDqZQN79?n?!Aj!Nj`YMO9?j>uqI9-Tex+nJD z%e0#Yca6(zqGUR|KITa?9x-#C0!JKJHO(+fy@1!B$%ZwJwncQW7vGYv?~!^`#L~Um zOL++>4qmqW`0Chc0T23G8|vO)tK=Z2`gvS4*qpqhIJCEv9i&&$09VO8YOz|oZ+ubd zNXVdLc&p=KsSgtmIPLN69P7xYkYQ1vJ?u1g)T!6Ru`k2wkdj*wDC)VryGu2=yb0?F z>q~~e>KZ0d_#7f3UgV%9MY1}vMgF{B8yfE{HL*pMyhYF)WDZ^^3vS8F zGlOhs%g_~pS3=WQ#494@jAXwOtr^Y|TnQ5zki>qRG)(oPY*f}U_=ip_{qB0!%w7~G zWE!P4p3khyW-JJnE>eECuYfI?^d366Shq!Wm#x&jAo>=HdCllE$>DPO0N;y#4G)D2y#B@5=N=+F%Xo2n{gKcPcK2!hP*^WSXl+ut; zyLvVoY>VL{H%Kd9^i~lsb8j4>$EllrparEOJNT?Ym>vJa$(P^tOG)5aVb_5w^*&M0 zYOJ`I`}9}UoSnYg#E(&yyK(tqr^@n}qU2H2DhkK-`2He% zgXr_4kpXoQHxAO9S`wEdmqGU4j=1JdG!OixdqB4PPP6RXA}>GM zumruUUH|ZG2$bBj)Qluj&uB=dRb)?^qomw?Z$X%#D+Q*O97eHrgVB2*mR$bFBU`*} zIem?dM)i}raTFDn@5^caxE^XFXVhBePmH9fqcTi`TLaXiueH=@06sl}>F%}h9H_e9 z>^O?LxM1EjX}NVppaO@NNQr=AtHcH-BU{yBT_vejJ#J)l^cl69Z7$sk`82Zyw7Wxt z=~J?hZm{f@W}|96FUJfy65Gk8?^{^yjhOahUMCNNpt5DJw}ZKH7b!bGiFY9y6OY&T z_N)?Jj(MuLTN36ZCJ6I5Xy7uVlrb$o*Z%=-)kPo9s?<^Yqz~!Z* z_mP8(unFq65XSi!$@YtieSQ!<7IEOaA9VkKI?lA`*(nURvfKL8cX}-+~uw9|_5)uC2`ZHcaeX7L8aG6Ghleg@F9aG%X$#g6^yP5apnB>YTz&EfS{q z9UVfSyEIczebC)qlVu5cOoMzS_jrC|)rQlAzK7sfiW0`M8mVIohazPE9Jzn*qPt%6 zZL8RELY@L09B83@Be;x5V-IHnn$}{RAT#<2JA%ttlk#^(%u}CGze|1JY5MPhbfnYG zIw%$XfBmA-<_pKLpGKwbRF$#P;@_)ech#>vj25sv25VM$ouo)?BXdRcO{)*OwTw)G zv43W~T6ekBMtUD%5Bm>`^Ltv!w4~65N!Ut5twl!Agrzyq4O2Fi3pUMtCU~>9gt_=h-f% z;1&OuSu?A_sJvIvQ+dZNo3?m1%b1+s&UAx?8sUHEe_sB7zkm4R%6)<@oYB_i5>3Ip zIA+?jVdX|zL{)?TGpx+=Ta>G80}0}Ax+722$XFNJsC1gcH56{8B)*)eU#r~HrC&}` z|EWW92&;6y;3}!L5zXa385@?-D%>dSvyK;?jqU2t_R3wvBW;$!j45uQ7tyEIQva;Db}r&bR3kqNSh)Q_$MJ#Uj3Gj1F;)sO|%6z#@<+ zi{pbYsYS#u`X$Nf($OS+lhw>xgjos1OnF^$-I$u;qhJswhH~p|ab*nO>zBrtb0ndn zxV0uh!LN`&xckTP+JW}gznSpU492)u+`f{9Yr)js`NmfYH#Wdtradc0TnKNz@Su!e zu$9}G_=ku;%4xk}eXl>)KgpuT>_<`Ud(A^a++K&pm3LbN;gI}ku@YVrA%FJBZ5$;m zobR8}OLtW4-i+qPPLS-(7<>M{)rhiPoi@?&vDeVq5%fmZk=mDdRV>Pb-l7pP1y6|J z8I>sF+TypKV=_^NwBU^>4JJq<*14GLfM2*XQzYdlqqjnE)gZsPW^E@mp&ww* zW9i>XL=uwLVZ9pO*8K>t>vdL~Ek_NUL$?LQi5sc#1Q-f6-ywKcIT8Kw?C(_3pbR`e|)%9S-({if|E+hR2W!&qfQ&UiF^I!|M#xhdWsenv^wpKCBiuxXbnp85`{i|;BM?Ba`lqTA zyRm=UWJl&E{8JzYDHFu>*Z10-?#A8D|5jW9Ho0*CAs0fAy~MqbwYuOq9jjt9*nuHI zbDwKvh)5Ir$r!fS5|;?Dt>V+@F*v8=TJJF)TdnC#Mk>+tGDGCw;A~^PC`gUt*<(|i zB{{g{`uFehu`$fm4)&k7`u{xIV)yvA(%5SxX9MS80p2EKnLtCZ>tlX>*Z6nd&6-Mv$5rHD*db;&IBK3KH&M<+ArlGXDRdX1VVO4)&R$f4NxXI>GBh zSv|h>5GDAI(4E`@F?EnW zS>#c&Gw6~_XL`qQG4bK`W*>hek4LX*efn6|_MY+rXkNyAuu?NxS%L7~9tD3cn7&p( zCtfqe6sjB&Q-Vs7BP5+%;#Gk};4xtwU!KY0XXbmkUy$kR9)!~?*v)qw00!+Yg^#H> zc#8*z6zZo>+(bud?K<*!QO4ehiTCK&PD4G&n)Tr9X_3r-we z?fI+}-G~Yn93gI6F{}Dw_SC*FLZ)5(85zp4%uubtD)J)UELLkvGk4#tw&Tussa)mTD$R2&O~{ zCI3>fr-!-b@EGRI%g0L8UU%%u_<;e9439JNV;4KSxd|78v+I+8^rmMf3f40Jb}wEszROD?xBZu>Ll3;sUIoNxDK3|j3*sam2tC@@e$ z^!;+AK>efeBJB%ALsQ{uFui)oDoq()2USi?n=6C3#eetz?wPswc={I<8x=(8lE4EIsUfyGNZ{|KYn1IR|=E==f z(;!A5(-2y^2xRFCSPqzHAZn5RCN_bp22T(KEtjA(rFZ%>a4@STrHZflxKoqe9Z4@^ zM*scx_y73?Q{vt6?~WEl?2q*;@8 z3M*&@%l)SQmXkcUm)d@GT2#JdzhfSAP9|n#C;$E8X|pwD!r#X?0P>0ZisQ~TNqupW z*lUY~+ikD`vQb?@SAWX#r*Y+;=_|oacL$2CL$^(mV}aKO77pg}O+-=T1oLBT5sL2i z42Qth2+0@C`c+*D0*5!qy26sis<9a7>LN2{z%Qj49t z=L@x`4$ALHb*3COHoT?5S_c(Hs}g!V>W^=6Q0}zaubkDn)(lTax0+!+%B}9Vqw6{H zvL|BRM`O<@;eVi1DzM!tXtBrA20Ce@^Jz|>%X-t`vi-%WweXCh_LhI#bUg2*pcP~R z*RuTUzBKLXO~~uMd&o$v3@d0shHfUjC6c539PE6rF&;Ufa(Rw@K1*m7?f5)t`MjH0 z)_V(cajV5Am>f!kWcI@5rE8t6$S>5M=k=aRZROH6fA^jJp~2NlR4;Q2>L$7F#RT#9 z>4@1RhWG`Khy>P2j1Yx^BBL{S`niMaxlSWV-JBU0-T9zZ%>7mR3l$~QV$({o0;jTI ze5=cN^!Bc2bT|BcojXp~K#2cM>OTe*cM{Kg-j*CkiW)EGQot^}s;cy8_1_@JA0Whq zlrNr+R;Efa+`6N)s5rH*|E)nYZ3uqkk2C(E7@A|3YI`ozP~9Lexx#*1(r8luq+YPk z{J}c$s` zPM35Fx(YWB3Z5IYnN+L_4|jaR(5iWJi2~l&xy}aU7kW?o-V*6Av2wyZTG!E2KSW2* zGRLQkQU;Oz##ie-Z4fI)WSRxn$(ZcD;TL+;^r=a4(G~H3ZhK$lSXZj?cvyY8%d9JM zzc3#pD^W_QnWy#rx#;c&N@sqHhrnHRmj#i;s%zLm6SE(n&BWpd&f7>XnjV}OlZntI70fq%8~9<7 zMYaw`E-rp49-oC1N_uZTo)Cu%RR2QWdHpzQIcNsoDp`3xfP+`gI?tVQZ4X={qU?(n zV>0ASES^Xuc;9JBji{)RnFL(Lez;8XbB1uWaMp@p?7xhXk6V#!6B@aP4Rz7-K%a>i z?fvf}va_DGUXlI#4--`A3qK7J?-HwnG7O~H2;zR~RLW)_^#La!=}+>KW#anZ{|^D3 B7G?kd literal 0 HcmV?d00001 diff --git a/sources/bower.json b/sources/bower.json new file mode 100644 index 0000000..72a4e1c --- /dev/null +++ b/sources/bower.json @@ -0,0 +1,17 @@ +{ + "name": "glowing-bear", + "description": "A webclient for WeeChat", + "version": "0.6.0", + "homepage": "https://github.com/glowing-bear/glowing-bear", + "license": "GPLv3", + "private": true, + "dependencies": { + "angular": "1.4.x", + "angular-route": "1.4.x", + "angular-sanitize": "1.4.x", + "angular-touch": "1.4.x", + "angular-loader": "1.4.x", + "angular-mocks": "1.4.x", + "html5-boilerplate": "~4.3.0" + } +} diff --git a/sources/css/glowingbear.css b/sources/css/glowingbear.css new file mode 100644 index 0000000..33719e2 --- /dev/null +++ b/sources/css/glowingbear.css @@ -0,0 +1,852 @@ +html, +body { + height: 100%; +/* The html and body elements cannot have any padding or margin. */ +} +.no-overflow { + overflow: hidden; +} + +.mobile { + display: none; +} + +a { + cursor: pointer; +} + +.version { + margin-right: 1em; +} + +.hidden-bracket { + position: absolute; + left: -1000px; + overflow: hidden; +} + +td.prefix { + text-align: right; + vertical-align: top; + padding: 1px 5px 1px 1px; + white-space: pre; + border-right: 1px solid #444; +} +td.message { + overflow: hidden; + vertical-align: top; + width: 100%; + padding: 1px 1px 1px 5px; + +-ms-word-break: break-all; + word-break: break-all; + + /* Non standard for webkit */ + word-break: break-word; + +-webkit-hyphens: auto; + -moz-hyphens: auto; + -ms-hyphens: auto; + hyphens: auto; + +} + +#readmarker { + margin-top: 5px; + margin-bottom: 5px; + border-top: 1px solid; + border-bottom: 1px solid; + height: 2px; +} + +.text { + white-space: pre-wrap; +} + +#sendMessage { + width: 100%; + height: 35px; + resize: none; +} + +#sendMessage:focus, #sendMessage:active { + border-bottom: 2px solid #555; +} + +.input-group-addon, .input-group-btn { + vertical-align: top; +} + +.footer button { + border-radius: 0; +} +.panel input, .panel .input-group { + max-width: 300px; +} +input[type=text], input[type=password], #sendMessage { + border: 0; + border-radius: 0; + margin-bottom: 5px !important; +} + +.btn-send-image { + position: relative; + overflow: hidden; + cursor: pointer; +} + +.imgur-upload { + position: absolute; + bottom: 0; + right: 0; + cursor: inherit; + font-size: 1000px !important; + height: 300px; + margin: 0; + padding: 0; + opacity: 0; + filter: ~"alpha(opacity=0)"; +} + +.input-group { + width: 100%; +} +.row { + margin: 0px; + max-width: 300px; +} +.no-gutter [class*="col"] { + padding: 0px +} +.col-sm-9 { + padding-right: 5px !important; +} +.glyphicon { + top: 0; /* Fixes alignment issue in top bar */ +} +#topbar { + position: fixed; + width: 100%; + height: 35px; + max-height: 35px; + z-index: 3; + line-height: 35px; + white-space: nowrap; +} +#topbar .brand { + float: left; + height: 35px; +} +#topbar .brand a { + display: inline-block; + padding: 0 10px; +} +#topbar .brand img { + height: 32px; + padding: 2px; +} +#topbar .brand button { + position: absolute; + line-height: 15px; + font-size: 9pt; + margin-left: 10px +} +#topbar .title { + position: fixed; + left: 145px; /* sidebar */ + overflow: hidden; +} + +#topbar .actions { + margin-left: 5px; + padding-left: 5px; + margin-right: 0; + padding-right: 5px; + height: 35px; + line-height: 35px; + font-size: 22px; + position: fixed; + right: 0; +} +#topbar .actions > * { + padding: 0 5px; + display: inline-block; +} +#topbar .actions .glyphicon { + line-height: 35px; + top: 0; +} +#topbar .dropdown-menu form { + padding-left: 6px; + padding-right: 6px; +} + +.upload-error { + width: 100%; + position: absolute; + top: 0; + left: 0; + z-index: 4; +} + +#sidebar { + position: fixed; + width: 140px; + min-height: 100%; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + padding-top: 35px; /* topbar */ + padding-bottom: 1px; /* need to force a padding here */ + font-size: smaller; + transition:0.2s ease-in-out; + z-index: 2; +} + +#sidebar[data-state=visible] { + left: 0px; +} + +#sidebar form { +} +#sidebar.ng-hide-add, #sidebar.ng-hide-remove { + /* this needs to be here to make it visible during the animation + since the .ng-hide class is already on the element rendering + it as hidden. */ + display:block!important; +} + +#sidebar .badge { + border-radius: 0; + margin-right: -10px; + padding: 4px 7px; +} + +#sidebar ul.indented li.indent span.buffername { + padding-left: 10px; +} + +#sidebar.ng-hide { + width: 0; +} + +#nicklist { + position: fixed; + width: 100px; + min-height: 100%; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + right: 0; + top: 0; + padding-top: 39px; + padding-left: 5px; + padding-bottom: 35px; + z-index: 2; +} +#nicklist ul { + padding: 0; + margin: 0; +} +#nicklist li, +#nicklist a { + display: block; +} +#nicklist a { + text-decoration: none; +} + +#connection-infos { + float: left; + max-width: 10%; + padding-left: 5px; + font-size: 12px; + overflow: hidden; +} + +.nav-pills li { + min-height: 20px; +} +.nav-pills li+li { + margin-top: 0; +} +.nav-pills > li > a { + border-radius: 0; + color: #ddd; + padding: 5px 10px; +} +.nav-pills > li > a:hover, .nav-pills > li > a:hover span { + color: #222; +} +/* fix for mobile firefox which ignores :hover */ +.nav-pills > li > a:active, .nav-pills > li > a:active span { + text-decoration: none; + background-color: #eee; + color: #222; +} + +.nav-pills > li > a { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.content { + height: 100%; + min-height: 100%; +} + +#bufferlines { + position: relative; + height: 100%; + overflow-y: auto; + width: auto; + bottom: 35px; /* input bar */ + padding-top: 42px; /* topbar */ + padding-bottom: 7px; + -webkit-transition:0.2 ease-in-out all; + transition:0.2s ease-in-out all; + -webkit-overflow-scrolling: touch; /* Native scroll on ios */ +} +#bufferlines > table { + margin-top: 35px; + width: 100%; +} +tr.bufferline { + line-height: 100%; +} + +td.time { + padding: 1px 5px 1px 1px; + vertical-align: top; +} + +.withnicklist { + margin-right: 100px !important; /* nicklist */ +} +.content[sidebar-state=visible] #bufferlines { + margin-left: 145px; /* sidebar */ +} +#bufferlines .btn { + font-family: sans-serif; +} + +#reconnect { + top: 35px; + position: fixed; + z-index: 9999; + width: 80%; + margin: 0; + padding: 5px; + left: 10%; +} +#reconnect a { + color: white; +} + +.footer { + position: fixed; + bottom: 0; + height: 35px; + width: 100%; + -webkit-transition:0.2s ease-in-out all; + transition:0.2s ease-in-out all; + z-index: 1; +} +.content[sidebar-state=visible] .footer { + margin-left: 0; + padding-left: 145px; +} +.footer.withnicklist { + padding-right: 100px; +} + +#inputform { + position: relative; +} + +#imgur-upload-progress { + width: 100%; + height: auto; + position: absolute; + bottom: 100%; + left: 0; +} + +.imgur-progress-bar { + width: 0%; + height: 5px; + margin-top: 1px; + background: #428BCA; +} + +/* fix for mobile firefox which ignores :hover */ +.nav-pills > li > a:active, .nav-pills > li > a:active span { + text-decoration: none; +} + +[ng-click], +[data-ng-click], +[x-ng-click] { + cursor: pointer; +} +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-thumb:vertical { + height: 15px; +} + +div.embed * { + max-width: 100%; +} + +/* not for all img embeds so as not to affect the yr plugin (302px) */ +div.embed img.embed { + max-height: 300px; + max-width: 100%; +} + +video.embed { + max-width: 100%; +} + +div.colourbox { + display: inline-block; + border-radius: 3px; + border: 1px solid #bbb; + width: 14px; + height: 14px; + margin-bottom: -2px; +} + + +table.notimestamp td.time { + display: none !important; +} + +table.notimestampseconds td.time span.seconds { + display: none !important; +} + +#sidebar .showquickkeys .buffer .buffer-quick-key { + transition: all ease-in-out 0.5s; + -webkit-transition: all ease-in-out 0.5s; + transition-delay: 0.2s; + -webkit-transition-delay: 0.2s; + opacity: 0.7; +} +#sidebar .buffer .buffer-quick-key { + margin-left: -0.7em; + margin-right: -0.2em; + font-size: smaller; + transition: all ease-in-out 0.5s; + -webkit-transition: all ease-in-out 0.5s; + opacity: 0; + text-shadow: -1px 0px 4px rgba(255, 255, 255, 0.4), + 0px -1px 4px rgba(255, 255, 255, 0.4), + 1px 0px 4px rgba(255, 255, 255, 0.4), + 0px 1px 4px rgba(255, 255, 255, 0.4); + vertical-align: baseline; + display: inline-block; + width: 1em; + align: right; +} + +.gb-modal { + z-index: 1000; + height: 100%; + overflow-y: scroll; + position: absolute; + top: 0; + left: 0; + width: 100%; +} + +.gb-modal[data-state=hidden] { + transition: .2s ease-in-out; + visibility: hidden; + opacity: 0; +} + +.gb-modal[data-state=visible] { + transition: .2s ease-in-out; + visibility: visible; + opacity: 1; +} + +.gb-modal[data-state=hidden] .modal-dialog { + transition: top .3s ease-in; + top: -150px; +} + +.gb-modal[data-state=visible] .modal-dialog { + transition: top .3s ease-out; + top: 0px; +} +.gb-modal .backdrop { + z-index: 999; + position: fixed; + top: 0; + height: 100%; + width: 100%; + overflow: none; +} + +.gb-modal .modal-dialog { + z-index: 1001; + position: absolute; + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + top: 35px; +} +.gb-modal[ng-click], .gb-modal div[ng-click] { + cursor: default; +} + +.gb-modal ul { + list-style: none; + padding-left: 15px; +} +.gb-modal li { + font-size: larger; + margin-bottom: 10px; +} +.gb-modal li li { + font-size: medium; +} +.modal-header { + padding-top: 23px; + border-bottom: 0; +} + +.standard-labels label { + font-weight: normal; + text-align: left; +} + + +h2 { + padding-bottom: 5px; + height: 72px; +} + +h2 img { + padding-right: 5px; + float: left; + height: 72px; +} + +h2 span, h2 small { + padding: 5px 0 0 0; + display: block; +} + +.panel[data-state=active] .panel-collapse { + transition: max-height 0.5s; + max-height: 60em; + height: auto; + display: block; +} +.panel[data-state=collapsed] .panel-collapse { + transition: max-height 0.5s; + max-height: 0; +} +.panel[data-state=collapsed] { + border: 0px solid transparent; +} +.panel .panel-title:before { + display: inline-block; + font-size: 22px; + line-height: 20px; + margin: -3px 5px -3px 0; +} +.panel[data-state=active] .panel-title:before { + content: "–"; +} +.panel[data-state=collapsed] .panel-title:before { + content: "+"; +} + +/* fix for firefox being stupid */ +@-moz-document url-prefix() { + .panel[data-state=collapsed] .panel-collapse * { + display: none; + } +} +/* bold hash before channels */ +li.buffer.channel a span:last-of-type:before, #topbar .title .channel:before { + color: #888; + font-weight: bold; +} + +li.buffer.channel_hash a span:last-of-type:before, #topbar .title .channel_hash:before { + content: '#'; +} + +li.buffer.channel_plus a span:last-of-type:before, #topbar .title .channel_plus:before { + content: '+'; +} + +li.buffer.channel_ampersand a span:last-of-type:before, #topbar .title .channel_ampersand:before { + content: '&'; +} + +li.buffer.channel.active a span:last-of-type:before { + color: #aaa; +} + +li.buffer.indent.private a { + padding-left: 17px; +} + +.make-thinner { + padding-right: -15px; +} + +.settings-help { + display: block; + margin: -5px 0 -3px 19px; + font-size: small; +} + +.unselectable { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.emojione { + font-size: inherit; + height: 1em; + width: 1.1em; + min-height: 16px; + min-width: 16px; + display: inline-block; + margin: -.2ex .15em .2ex; + line-height: normal; + vertical-align: middle; +} +img.emojione { + width: auto; +} + +.glyphicon-spin { + -webkit-animation: spin 1000ms infinite linear; + animation: spin 1000ms infinite linear; +} +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} + +@media (min-width: 1400px) { + #sidebar[data-state=visible], #sidebar { + width: 200px; + } + .content[sidebar-state="visible"] #bufferlines { + margin-left: 205px; + } + #topbar .title { + left: 205px; + } + .content[sidebar-state=visible] .footer { + padding-left: 200px; + } + + .nav-pills { + font-size: 14px; + } + + .nav-pills li a { + padding: 10px 15px; + } + + #nicklist { + width: 140px; + } + .withnicklist { + margin-right: 140px !important; /* nicklist */ + } + .footer.withnicklist { + padding-right: 148px !important; + } +} + +/* */ +/* Mobile layout */ +/* */ +@media (max-width: 968px) { + + .mobile { + display: inherit; + } + + .desktop { + display: none; + } + + #bufferlines table { + border-collapse: separate; + border-spacing: 2px 3px; + } + + #sidebar { + font-size: normal; + bottom: 0px; + top: 0px; + padding-bottom: 35px; + width: 200px; + } + + #sidebar.in, #sidebar.collapsing { + -webkit-box-shadow: 0px 0px 120px #000; + box-shadow: 0px 0px 120px #000; + bottom: 0px; + } + + #sidebar[data-state=visible] { + transform: translate(0,0); + -webkit-transform: translate(0,0); /* Safari */ + } + + #sidebar[data-state=hidden] { + transform: translate(-200px,0); + -webkit-transform: translate(-200px,0); + } + + .content[sidebar-state=visible] #bufferlines, .content[sidebar-state=visible] .footer { + margin-left: 0px; + transform: translate(200px,0); + -webkit-transform: translate(200px,0); + } + + #topbar .title { + left: 40px; + right: 60px; + text-align: center; + font-size: 18px; + } + + #topbar .brand img { + height: 28px; + } + + #topbar .badge { + display: none; + } + + + #bufferlines, #nicklist { + position: relative; + min-height: 0; + margin-left: 0; + max-width: 100%; + border: 0; + } + + #nicklist { + height: auto; + padding: 35px 7px 35px 10px; + text-align: center; + position: fixed; + margin-top: 10px; + bottom: 0px; + } + + .navbar-fixed-bottom { + margin: 0; + } + + .navbar { + min-height: 0%; + } + + .nav-pills { + font-size: 14px; + } + + .nav-pills li a { + padding: 10px 15px; + } + + #bufferlines { + height: 100%; + padding-bottom: 0; + } + + #bufferlines tr.bufferline { + display: block; + overflow: hidden; + } + + #bufferlines td.time { + display: inline-block; + padding-right: 3px; + font-size: 0.8em; + } + + #bufferlines td.time span.date { + display: block; + margin-bottom: -1px; + } + + #bufferlines td.prefix { + display: inline; + padding-right: 5px; + border: 0; + font-weight: bold; + font-size: 1.06em; + } + + #bufferlines td.message { + display: inline; + padding: 0px !important; + } + + .gb-modal .modal-dialog { + margin: 20px 2%; + width: 96%; + } + + .footer { + padding-left: 0px !important; + padding-right: 0px !important; + width: 100% !important; + } + + .footer.withnicklist { + padding-right: 108px !important; + } + + ::-webkit-scrollbar { + width: 5px; + height: 5px; + } + .col-sm-9 { + padding-right: 0px !important; + } +} diff --git a/sources/css/themes/black.css b/sources/css/themes/black.css new file mode 100644 index 0000000..08ca482 --- /dev/null +++ b/sources/css/themes/black.css @@ -0,0 +1,22 @@ +@import "dark.css"; + +body, .modal-content { + background-color: #000; +} + +#topbar, #sidebar, .panel, .dropdown-menu, #topbar .actions { + background: #080808; +} + +.nav-pills li:nth-child(2n) { + background: #000; +} + +.form-control option, input.form-control, select.form-control { + color: #ccc; + background: #080808; +} + +.close, .close:hover, .close:focus { + color: #ddd; +} diff --git a/sources/css/themes/dark.css b/sources/css/themes/dark.css new file mode 100644 index 0000000..8d303aa --- /dev/null +++ b/sources/css/themes/dark.css @@ -0,0 +1,2126 @@ +body { + background-color: #181818; + color: #ddd; +} + +.form-control { + color: #ccc; + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset; + background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3); + border: 0px none; +} + +.form-control option { + color: #eee; + background: #282828; +} + +html { + background-color: inherit; +} + +.repeated-time .cob-chat_time, +.repeated-time .cob-chat_time_delimiters { + background-color: transparent; + color: #333; +} + +.nav-pills > li.active > a { + background-color: #555; +} + +/* fix for mobile firefox which ignores :hover */ +.nav-pills > li > a:active, .nav-pills > li > a:active span, .nav-pills > li.active > a:hover { + background-color: #eee; + color: #222; +} + +tr.bufferline:hover { + background-color: #222222; +} + +.danger, .alert-danger, .badge .alert-danger, .badge.danger { + background-color: rgb(217, 83, 79); + color: #ddd; +} +.alert-danger { + border-color: #121212; + color: black; +} + +.btn-default { + background-color: #555; + border-color: #444; +} +.btn-default:hover { + background-color: #666; + border-color: #555; +} + +li.notification { + color: green; +} + +::-webkit-scrollbar-track-piece { + background-color: black; +} +::-webkit-scrollbar-thumb:vertical { + height: 15px; + background: rgba(255,255,255,0.5); +} + +.gb-modal .backdrop { + background-color:rgba(0, 0, 0, 0.5) +} + +input[type=text], input[type=password], #sendMessage, .badge { + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset; +} + +input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-send-image { + color: #ccc; + background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3); +} + +.btn-send:hover, .btn-send:focus, +.btn-send-image:hover, .btn-send-image:focus { + background-color: #555; + color: white; +} + +#connection-infos { + color: #aaa; +} + +.nav-pills li:nth-child(2n) { + background: #232323; +} + +.nav-pills > li > a { + color: #ddd; +} + +.nav-pills > li > a:hover, .nav-pills > li > a:hover span { + color: #222; +} + +.color-light-green { + color: chartreuse; +} + +.color-27 { + color: deepskyblue; +} + +#topbar .actions { + background: #282828; + color: #666; +} + +#topbar, #sidebar, .panel, .dropdown-menu, .modal-content { + background: #282828; +} + +#nicklist a:hover { + background: #3b3b3b; +} + +.horizontal-line { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; + box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; + border-bottom: 1px solid #121212; +} +.vertical-line { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; + box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; + border-right: 1px solid #121212; +} +.vertical-line-left { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; + box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; + border-left: 1px solid #121212; +} +.panel-group .panel-heading + .panel-collapse .panel-body, .modal-body, .modal-header, .modal-footer { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; + box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; + border-top: 1px solid #121212; +} + +#readmarker { + border-top-color: rgba(255, 255, 255, 0.3); + border-bottom-color: #121212; +} + +/****************************/ +/* Weechat colors and style */ +/****************************/ +/* style options, foreground */ +.cof-separator { + color: #68b5d4; +} +.cof-chat { + color: #d9d9d9; +} +.cof-chat_time { + color: #f7f7f7; +} +.cof-chat_time_delimiters { + color: #cc843b; +} +.cof-chat_prefix_error { + color: #ffe377; +} +.cof-chat_prefix_network { + color: #fbb1f9; +} +.cof-chat_prefix_action { + color: #f7f7f7; +} +.cof-chat_prefix_join { + color: #cdee69; +} +.cof-chat_prefix_quit { + color: #c75646; +} +.cof-chat_prefix_more { + color: #fbb1f9; +} +.cof-chat_prefix_suffix { + color: #cdee69; +} +.cof-chat_buffer { + color: #f7f7f7; +} +.cof-chat_server { + color: #cc843b; +} +.cof-chat_channel { + color: #f7f7f7; +} +.cof-chat_nick { + color: #77dfd8; +} +.cof-chat_nick_self { + color: #ffffff; + font-weight: bold; +} +.cof-chat_nick_other { + color: #77dfd8; +} +.cof-invalid { + /* should never happen */ + color: #f7f7f7; + background-color: transparent; +} +.cof-chat_host { + color: #77dfd8; +} +.cof-chat_delimiters { + color: #cdee69; +} +.cof-chat_highlight { + color: #ffe377; + background-color: #c8a0d1; +} +.cof-chat_read_marker { + color: #fbb1f9; +} +.cof-chat_text_found { + color: #ffe377; +} +.cof-chat_value { + color: #77dfd8; +} +.cof-chat_prefix_buffer { + color: #cc843b; +} +.cof-chat_tags { + color: #c75646; +} +.cof-chat_inactive_window { + color: #5d5d5d; +} +.cof-chat_inactive_buffer { + color: #5d5d5d; +} +.cof-chat_prefix_buffer_inactive_buffer { + color: #5d5d5d; +} +.cof-chat_nick_offline { + color: #5d5d5d; +} +.cof-chat_nick_offline_highlight { + color: #f7f7f7; + background-color: #5d5d5d; +} +.cof-chat_nick_prefix { + color: #cdee69; +} +.cof-chat_nick_suffix { + color: #cdee69; +} +.cof-emphasis { + color: #ffe377; + background-color: #c8a0d1; +} +.cof-chat_day_change { + color: #77dfd8; +} + +/* style options, background */ +.cob-separator { +} +.cob-chat { +} +.cob-chat_time { + color: #999; +} +.cob-chat_time_delimiters { +} +.cob-chat_prefix_error { +} +.cob-chat_prefix_network { +} +.cob-chat_prefix_action { +} +.cob-chat_prefix_join { +} +.cob-chat_prefix_quit { +} +.cob-chat_prefix_more { +} +.cob-chat_prefix_suffix { +} +.cob-chat_buffer { +} +.cob-chat_server { +} +.cob-chat_channel { +} +.cob-chat_nick { +} +.cob-chat_nick_self { +} +.cob-chat_nick_other { +} +.cob-invalid { +} +.cob-chat_host { +} +.cob-chat_delimiters { +} +.cob-chat_highlight { + background-color: #c8a0d1; +} +.cob-chat_read_marker { +} +.cob-chat_text_found { +} +.cob-chat_value { +} +.cob-chat_prefix_buffer { +} +.cob-chat_tags { +} +.cob-chat_inactive_window { +} +.cob-chat_inactive_buffer { +} +.cob-chat_prefix_buffer_inactive_buffer { +} +.cob-chat_nick_offline { +} +.cob-chat_nick_offline_highlight { + background-color: #5d5d5d; +} +.cob-chat_nick_prefix { +} +.cob-chat_nick_suffix { +} +.cob-emphasis { + background-color: #c8a0d1; +} +.cob-chat_day_change { +} + +/* style options, attributes */ +.coa-separator { +} +.coa-chat { +} +.coa-chat_time { +} +.coa-chat_time_delimiters { +} +.coa-chat_prefix_error { +} +.coa-chat_prefix_network { +} +.coa-chat_prefix_action { +} +.coa-chat_prefix_join { +} +.coa-chat_prefix_quit { +} +.coa-chat_prefix_more { +} +.coa-chat_prefix_suffix { +} +.coa-chat_buffer { +} +.coa-chat_server { +} +.coa-chat_channel { +} +.coa-chat_nick { +} +.coa-chat_nick_self { +} +.coa-chat_nick_other { +} +.coa-invalid { +} +.coa-chat_host { +} +.coa-chat_delimiters { +} +.coa-chat_highlight { +} +.coa-chat_read_marker { +} +.coa-chat_text_found { +} +.coa-chat_value { +} +.coa-chat_prefix_buffer { +} +.coa-chat_tags { +} +.coa-chat_inactive_window { +} +.coa-chat_inactive_buffer { +} +.coa-chat_prefix_buffer_inactive_buffer { +} +.coa-chat_nick_offline { +} +.coa-chat_nick_offline_highlight { +} +.coa-chat_nick_prefix { +} +.coa-chat_nick_suffix { +} +.coa-emphasis { +} +.coa-chat_day_change { +} + +/* WeeChat colors, foreground */ +.cwf-default { + color: #d9d9d9; +} +.light-theme .cwf-default { + color: #282828; +} +.cwf-black { + color: #000000; +} +.cwf-darkgray { + color: #5d5d5d; +} +.cwf-red { + color: #c75646; +} +.cwf-lightred { + color: #e09690; +} +.cwf-green { + color: #8eb33b; +} +.cwf-lightgreen { + color: #cdee69; +} +.cwf-brown { + color: #b27232; +} +.cwf-yellow { + color: #ffe377; +} +.cwf-blue { + color: #72b3cc; +} +.cwf-lightblue { + color: #9cd9f0; +} +.cwf-magenta { + color: #c8a0d1; +} +.cwf-lightmagenta { + color: #fbb1f9; +} +.cwf-cyan { + color: #218693; +} +.cwf-lightcyan { + color: #77dfd8; +} +.cwf-gray { + color: #b0b0b0; +} +.cwf-white { + color: #f7f7f7; +} + +/* WeeChat colors, background */ +.cwb-default { + background-color: transparent; +} +.cwb-black { + background-color: #000000; +} +.cwb-darkgray { + background-color: #5d5d5d; +} +.cwb-red { + background-color: #c75646; +} +.cwb-lightred { + background-color: #e09690; +} +.cwb-green { + background-color: #8eb33b; +} +.cwb-lightgreen { + background-color: #cdee69; +} +.cwb-brown { + background-color: #b27232; +} +.cwb-yellow { + background-color: #ffe377; +} +.cwb-blue { + background-color: #72b3cc; +} +.cwb-lightblue { + background-color: #9cd9f0; +} +.cwb-magenta { + background-color: #c8a0d1; +} +.cwb-lightmagenta { + background-color: #fbb1f9; +} +.cwb-cyan { + background-color: #218693; +} +.cwb-lightcyan { + background-color: #77dfd8; +} +.cwb-gray { + background-color: #b0b0b0; +} +.cwb-white { + background-color: #f7f7f7; +} + +/* extended colors, foreground */ +.cef-0 { + color: #000000; /* 000 Black */ +} +.cef-1 { + color: #7f0000; /* 001 DarkRed */ +} +.cef-2 { + color: #00cd00; /* 002 DarkGreen */ +} +.cef-3 { + color: #fc7f00; /* 003 Orange */ +} +.cef-4 { + color: #0000ee; /* 004 DarkBlue */ +} +.cef-5 { + color: #cd00cd; /* 005 DarkMagenta */ +} +.cef-6 { + color: #00cdcd; /* 006 DarkCyan */ +} +.cef-7 { + color: #e5e5e5; /* 007 LightGrey */ +} +.cef-8 { + color: #7f7f7f; /* 008 DarkGrey */ +} +.cef-9 { + color: #ff0000; /* 009 LightRed */ +} +.cef-10 { + color: #00ff00; /* 010 LightGreen */ +} +.cef-11 { + color: #ffff00; /* 011 LightYellow */ +} +.cef-12 { + color: #5c5cff; /* 012 LightBlue */ +} +.cef-13 { + color: #ff00ff; /* 013 LightMagenta */ +} +.cef-14 { + color: #00ffff; /* 014 LightCyan */ +} +.cef-15 { + color: #ffffff; /* 015 White */ +} +.cef-16 { + color: #000000; /* 016 Grey0 */ +} +.cef-17 { + color: #00005f; /* 017 NavyBlue */ +} +.cef-18 { + color: #000087; /* 018 DarkBlue */ +} +.cef-19 { + color: #0000af; /* 019 Blue3 */ +} +.cef-20 { + color: #0000d7; /* 020 Blue3 */ +} +.cef-21 { + color: #0000ff; /* 021 Blue1 */ +} +.cef-22 { + color: #005f00; /* 022 DarkGreen */ +} +.cef-23 { + color: #005f5f; /* 023 DeepSkyBlue4 */ +} +.cef-24 { + color: #005f87; /* 024 DeepSkyBlue4 */ +} +.cef-25 { + color: #005faf; /* 025 DeepSkyBlue4 */ +} +.cef-26 { + color: #005fd7; /* 026 DodgerBlue3 */ +} +.cef-27 { + color: #005fff; /* 027 DodgerBlue2 */ +} +.cef-28 { + color: #008700; /* 028 Green4 */ +} +.cef-29 { + color: #00875f; /* 029 SpringGreen4 */ +} +.cef-30 { + color: #008787; /* 030 Turquoise4 */ +} +.cef-31 { + color: #0087af; /* 031 DeepSkyBlue3 */ +} +.cef-32 { + color: #0087d7; /* 032 DeepSkyBlue3 */ +} +.cef-33 { + color: #0087ff; /* 033 DodgerBlue1 */ +} +.cef-34 { + color: #00af00; /* 034 Green3 */ +} +.cef-35 { + color: #00af5f; /* 035 SpringGreen3 */ +} +.cef-36 { + color: #00af87; /* 036 DarkCyan */ +} +.cef-37 { + color: #00afaf; /* 037 LightSeaGreen */ +} +.cef-38 { + color: #00afd7; /* 038 DeepSkyBlue2 */ +} +.cef-39 { + color: #00afff; /* 039 DeepSkyBlue1 */ +} +.cef-40 { + color: #00d700; /* 040 Green3 */ +} +.cef-41 { + color: #00d75f; /* 041 SpringGreen3 */ +} +.cef-42 { + color: #00d787; /* 042 SpringGreen2 */ +} +.cef-43 { + color: #00d7af; /* 043 Cyan3 */ +} +.cef-44 { + color: #00d7d7; /* 044 DarkTurquoise */ +} +.cef-45 { + color: #00d7ff; /* 045 Turquoise2 */ +} +.cef-46 { + color: #00ff00; /* 046 Green1 */ +} +.cef-47 { + color: #00ff5f; /* 047 SpringGreen2 */ +} +.cef-48 { + color: #00ff87; /* 048 SpringGreen1 */ +} +.cef-49 { + color: #00ffaf; /* 049 MediumSpringGreen */ +} +.cef-50 { + color: #00ffd7; /* 050 Cyan2 */ +} +.cef-51 { + color: #00ffff; /* 051 Cyan1 */ +} +.cef-52 { + color: #5f0000; /* 052 DarkRed */ +} +.cef-53 { + color: #5f005f; /* 053 DeepPink4 */ +} +.cef-54 { + color: #5f0087; /* 054 Purple4 */ +} +.cef-55 { + color: #5f00af; /* 055 Purple4 */ +} +.cef-56 { + color: #5f00d7; /* 056 Purple3 */ +} +.cef-57 { + color: #5f00ff; /* 057 BlueViolet */ +} +.cef-58 { + color: #5f5f00; /* 058 Orange4 */ +} +.cef-59 { + color: #5f5f5f; /* 059 Grey37 */ +} +.cef-60 { + color: #5f5f87; /* 060 MediumPurple4 */ +} +.cef-61 { + color: #5f5faf; /* 061 SlateBlue3 */ +} +.cef-62 { + color: #5f5fd7; /* 062 SlateBlue3 */ +} +.cef-63 { + color: #5f5fff; /* 063 RoyalBlue1 */ +} +.cef-64 { + color: #5f8700; /* 064 Chartreuse4 */ +} +.cef-65 { + color: #5f875f; /* 065 DarkSeaGreen4 */ +} +.cef-66 { + color: #5f8787; /* 066 PaleTurquoise4 */ +} +.cef-67 { + color: #5f87af; /* 067 SteelBlue */ +} +.cef-68 { + color: #5f87d7; /* 068 SteelBlue3 */ +} +.cef-69 { + color: #5f87ff; /* 069 CornflowerBlue */ +} +.cef-70 { + color: #5faf00; /* 070 Chartreuse3 */ +} +.cef-71 { + color: #5faf5f; /* 071 DarkSeaGreen4 */ +} +.cef-72 { + color: #5faf87; /* 072 CadetBlue */ +} +.cef-73 { + color: #5fafaf; /* 073 CadetBlue */ +} +.cef-74 { + color: #5fafd7; /* 074 SkyBlue3 */ +} +.cef-75 { + color: #5fafff; /* 075 SteelBlue1 */ +} +.cef-76 { + color: #5fd700; /* 076 Chartreuse3 */ +} +.cef-77 { + color: #5fd75f; /* 077 PaleGreen3 */ +} +.cef-78 { + color: #5fd787; /* 078 SeaGreen3 */ +} +.cef-79 { + color: #5fd7af; /* 079 Aquamarine3 */ +} +.cef-80 { + color: #5fd7d7; /* 080 MediumTurquoise */ +} +.cef-81 { + color: #5fd7ff; /* 081 SteelBlue1 */ +} +.cef-82 { + color: #5fff00; /* 082 Chartreuse2 */ +} +.cef-83 { + color: #5fff5f; /* 083 SeaGreen2 */ +} +.cef-84 { + color: #5fff87; /* 084 SeaGreen1 */ +} +.cef-85 { + color: #5fffaf; /* 085 SeaGreen1 */ +} +.cef-86 { + color: #5fffd7; /* 086 Aquamarine1 */ +} +.cef-87 { + color: #5fffff; /* 087 DarkSlateGray2 */ +} +.cef-88 { + color: #870000; /* 088 DarkRed */ +} +.cef-89 { + color: #87005f; /* 089 DeepPink4 */ +} +.cef-90 { + color: #870087; /* 090 DarkMagenta */ +} +.cef-91 { + color: #8700af; /* 091 DarkMagenta */ +} +.cef-92 { + color: #8700d7; /* 092 DarkViolet */ +} +.cef-93 { + color: #8700ff; /* 093 Purple */ +} +.cef-94 { + color: #875f00; /* 094 Orange4 */ +} +.cef-95 { + color: #875f5f; /* 095 LightPink4 */ +} +.cef-96 { + color: #875f87; /* 096 Plum4 */ +} +.cef-97 { + color: #875faf; /* 097 MediumPurple3 */ +} +.cef-98 { + color: #875fd7; /* 098 MediumPurple3 */ +} +.cef-99 { + color: #875fff; /* 099 SlateBlue1 */ +} +.cef-100 { + color: #878700; /* 100 Yellow4 */ +} +.cef-101 { + color: #87875f; /* 101 Wheat4 */ +} +.cef-102 { + color: #878787; /* 102 Grey53 */ +} +.cef-103 { + color: #8787af; /* 103 LightSlateGrey */ +} +.cef-104 { + color: #8787d7; /* 104 MediumPurple */ +} +.cef-105 { + color: #8787ff; /* 105 LightSlateBlue */ +} +.cef-106 { + color: #87af00; /* 106 Yellow4 */ +} +.cef-107 { + color: #87af5f; /* 107 DarkOliveGreen3 */ +} +.cef-108 { + color: #87af87; /* 108 DarkSeaGreen */ +} +.cef-109 { + color: #87afaf; /* 109 LightSkyBlue3 */ +} +.cef-110 { + color: #87afd7; /* 110 LightSkyBlue3 */ +} +.cef-111 { + color: #87afff; /* 111 SkyBlue2 */ +} +.cef-112 { + color: #87d700; /* 112 Chartreuse2 */ +} +.cef-113 { + color: #87d75f; /* 113 DarkOliveGreen3 */ +} +.cef-114 { + color: #87d787; /* 114 PaleGreen3 */ +} +.cef-115 { + color: #87d7af; /* 115 DarkSeaGreen3 */ +} +.cef-116 { + color: #87d7d7; /* 116 DarkSlateGray3 */ +} +.cef-117 { + color: #87d7ff; /* 117 SkyBlue1 */ +} +.cef-118 { + color: #87ff00; /* 118 Chartreuse1 */ +} +.cef-119 { + color: #87ff5f; /* 119 LightGreen */ +} +.cef-120 { + color: #87ff87; /* 120 LightGreen */ +} +.cef-121 { + color: #87ffaf; /* 121 PaleGreen1 */ +} +.cef-122 { + color: #87ffd7; /* 122 Aquamarine1 */ +} +.cef-123 { + color: #87ffff; /* 123 DarkSlateGray1 */ +} +.cef-124 { + color: #af0000; /* 124 Red3 */ +} +.cef-125 { + color: #af005f; /* 125 DeepPink4 */ +} +.cef-126 { + color: #af0087; /* 126 MediumVioletRed */ +} +.cef-127 { + color: #af00af; /* 127 Magenta3 */ +} +.cef-128 { + color: #af00d7; /* 128 DarkViolet */ +} +.cef-129 { + color: #af00ff; /* 129 Purple */ +} +.cef-130 { + color: #af5f00; /* 130 DarkOrange3 */ +} +.cef-131 { + color: #af5f5f; /* 131 IndianRed */ +} +.cef-132 { + color: #af5f87; /* 132 HotPink3 */ +} +.cef-133 { + color: #af5faf; /* 133 MediumOrchid3 */ +} +.cef-134 { + color: #af5fd7; /* 134 MediumOrchid */ +} +.cef-135 { + color: #af5fff; /* 135 MediumPurple2 */ +} +.cef-136 { + color: #af8700; /* 136 DarkGoldenrod */ +} +.cef-137 { + color: #af875f; /* 137 LightSalmon3 */ +} +.cef-138 { + color: #af8787; /* 138 RosyBrown */ +} +.cef-139 { + color: #af87af; /* 139 Grey63 */ +} +.cef-140 { + color: #af87d7; /* 140 MediumPurple2 */ +} +.cef-141 { + color: #af87ff; /* 141 MediumPurple1 */ +} +.cef-142 { + color: #afaf00; /* 142 Gold3 */ +} +.cef-143 { + color: #afaf5f; /* 143 DarkKhaki */ +} +.cef-144 { + color: #afaf87; /* 144 NavajoWhite3 */ +} +.cef-145 { + color: #afafaf; /* 145 Grey69 */ +} +.cef-146 { + color: #afafd7; /* 146 LightSteelBlue3 */ +} +.cef-147 { + color: #afafff; /* 147 LightSteelBlue */ +} +.cef-148 { + color: #afd700; /* 148 Yellow3 */ +} +.cef-149 { + color: #afd75f; /* 149 DarkOliveGreen3 */ +} +.cef-150 { + color: #afd787; /* 150 DarkSeaGreen3 */ +} +.cef-151 { + color: #afd7af; /* 151 DarkSeaGreen2 */ +} +.cef-152 { + color: #afd7d7; /* 152 LightCyan3 */ +} +.cef-153 { + color: #afd7ff; /* 153 LightSkyBlue1 */ +} +.cef-154 { + color: #afff00; /* 154 GreenYellow */ +} +.cef-155 { + color: #afff5f; /* 155 DarkOliveGreen2 */ +} +.cef-156 { + color: #afff87; /* 156 PaleGreen1 */ +} +.cef-157 { + color: #afffaf; /* 157 DarkSeaGreen2 */ +} +.cef-158 { + color: #afffd7; /* 158 DarkSeaGreen1 */ +} +.cef-159 { + color: #afffff; /* 159 PaleTurquoise1 */ +} +.cef-160 { + color: #d70000; /* 160 Red3 */ +} +.cef-161 { + color: #d7005f; /* 161 DeepPink3 */ +} +.cef-162 { + color: #d70087; /* 162 DeepPink3 */ +} +.cef-163 { + color: #d700af; /* 163 Magenta3 */ +} +.cef-164 { + color: #d700d7; /* 164 Magenta3 */ +} +.cef-165 { + color: #d700ff; /* 165 Magenta2 */ +} +.cef-166 { + color: #d75f00; /* 166 DarkOrange3 */ +} +.cef-167 { + color: #d75f5f; /* 167 IndianRed */ +} +.cef-168 { + color: #d75f87; /* 168 HotPink3 */ +} +.cef-169 { + color: #d75faf; /* 169 HotPink2 */ +} +.cef-170 { + color: #d75fd7; /* 170 Orchid */ +} +.cef-171 { + color: #d75fff; /* 171 MediumOrchid1 */ +} +.cef-172 { + color: #d78700; /* 172 Orange3 */ +} +.cef-173 { + color: #d7875f; /* 173 LightSalmon3 */ +} +.cef-174 { + color: #d78787; /* 174 LightPink3 */ +} +.cef-175 { + color: #d787af; /* 175 Pink3 */ +} +.cef-176 { + color: #d787d7; /* 176 Plum3 */ +} +.cef-177 { + color: #d787ff; /* 177 Violet */ +} +.cef-178 { + color: #d7af00; /* 178 Gold3 */ +} +.cef-179 { + color: #d7af5f; /* 179 LightGoldenrod3 */ +} +.cef-180 { + color: #d7af87; /* 180 Tan */ +} +.cef-181 { + color: #d7afaf; /* 181 MistyRose3 */ +} +.cef-182 { + color: #d7afd7; /* 182 Thistle3 */ +} +.cef-183 { + color: #d7afff; /* 183 Plum2 */ +} +.cef-184 { + color: #d7d700; /* 184 Yellow3 */ +} +.cef-185 { + color: #d7d75f; /* 185 Khaki3 */ +} +.cef-186 { + color: #d7d787; /* 186 LightGoldenrod2 */ +} +.cef-187 { + color: #d7d7af; /* 187 LightYellow3 */ +} +.cef-188 { + color: #d7d7d7; /* 188 Grey84 */ +} +.cef-189 { + color: #d7d7ff; /* 189 LightSteelBlue1 */ +} +.cef-190 { + color: #d7ff00; /* 190 Yellow2 */ +} +.cef-191 { + color: #d7ff5f; /* 191 DarkOliveGreen1 */ +} +.cef-192 { + color: #d7ff87; /* 192 DarkOliveGreen1 */ +} +.cef-193 { + color: #d7ffaf; /* 193 DarkSeaGreen1 */ +} +.cef-194 { + color: #d7ffd7; /* 194 Honeydew2 */ +} +.cef-195 { + color: #d7ffff; /* 195 LightCyan1 */ +} +.cef-196 { + color: #ff0000; /* 196 Red1 */ +} +.cef-197 { + color: #ff005f; /* 197 DeepPink2 */ +} +.cef-198 { + color: #ff0087; /* 198 DeepPink1 */ +} +.cef-199 { + color: #ff00af; /* 199 DeepPink1 */ +} +.cef-200 { + color: #ff00d7; /* 200 Magenta2 */ +} +.cef-201 { + color: #ff00ff; /* 201 Magenta1 */ +} +.cef-202 { + color: #ff5f00; /* 202 OrangeRed1 */ +} +.cef-203 { + color: #ff5f5f; /* 203 IndianRed1 */ +} +.cef-204 { + color: #ff5f87; /* 204 IndianRed1 */ +} +.cef-205 { + color: #ff5faf; /* 205 HotPink */ +} +.cef-206 { + color: #ff5fd7; /* 206 HotPink */ +} +.cef-207 { + color: #ff5fff; /* 207 MediumOrchid1 */ +} +.cef-208 { + color: #ff8700; /* 208 DarkOrange */ +} +.cef-209 { + color: #ff875f; /* 209 Salmon1 */ +} +.cef-210 { + color: #ff8787; /* 210 LightCoral */ +} +.cef-211 { + color: #ff87af; /* 211 PaleVioletRed1 */ +} +.cef-212 { + color: #ff87d7; /* 212 Orchid2 */ +} +.cef-213 { + color: #ff87ff; /* 213 Orchid1 */ +} +.cef-214 { + color: #ffaf00; /* 214 Orange1 */ +} +.cef-215 { + color: #ffaf5f; /* 215 SandyBrown */ +} +.cef-216 { + color: #ffaf87; /* 216 LightSalmon1 */ +} +.cef-217 { + color: #ffafaf; /* 217 LightPink1 */ +} +.cef-218 { + color: #ffafd7; /* 218 Pink1 */ +} +.cef-219 { + color: #ffafff; /* 219 Plum1 */ +} +.cef-220 { + color: #ffd700; /* 220 Gold1 */ +} +.cef-221 { + color: #ffd75f; /* 221 LightGoldenrod2 */ +} +.cef-222 { + color: #ffd787; /* 222 LightGoldenrod2 */ +} +.cef-223 { + color: #ffd7af; /* 223 NavajoWhite1 */ +} +.cef-224 { + color: #ffd7d7; /* 224 MistyRose1 */ +} +.cef-225 { + color: #ffd7ff; /* 225 Thistle1 */ +} +.cef-226 { + color: #ffff00; /* 226 Yellow1 */ +} +.cef-227 { + color: #ffff5f; /* 227 LightGoldenrod1 */ +} +.cef-228 { + color: #ffff87; /* 228 Khaki1 */ +} +.cef-229 { + color: #ffffaf; /* 229 Wheat1 */ +} +.cef-230 { + color: #ffffd7; /* 230 Cornsilk1 */ +} +.cef-231 { + color: #ffffff; /* 231 Grey100 */ +} +.cef-232 { + color: #080808; /* 232 Grey3 */ +} +.cef-233 { + color: #121212; /* 233 Grey7 */ +} +.cef-234 { + color: #1c1c1c; /* 234 Grey11 */ +} +.cef-235 { + color: #262626; /* 235 Grey15 */ +} +.cef-236 { + color: #303030; /* 236 Grey19 */ +} +.cef-237 { + color: #3a3a3a; /* 237 Grey23 */ +} +.cef-238 { + color: #444444; /* 238 Grey27 */ +} +.cef-239 { + color: #4e4e4e; /* 239 Grey30 */ +} +.cef-240 { + color: #585858; /* 240 Grey35 */ +} +.cef-241 { + color: #626262; /* 241 Grey39 */ +} +.cef-242 { + color: #6c6c6c; /* 242 Grey42 */ +} +.cef-243 { + color: #767676; /* 243 Grey46 */ +} +.cef-244 { + color: #808080; /* 244 Grey50 */ +} +.cef-245 { + color: #8a8a8a; /* 245 Grey54 */ +} +.cef-246 { + color: #949494; /* 246 Grey58 */ +} +.cef-247 { + color: #9e9e9e; /* 247 Grey62 */ +} +.cef-248 { + color: #a8a8a8; /* 248 Grey66 */ +} +.cef-249 { + color: #b2b2b2; /* 249 Grey70 */ +} +.cef-250 { + color: #bcbcbc; /* 250 Grey74 */ +} +.cef-251 { + color: #c6c6c6; /* 251 Grey78 */ +} +.cef-252 { + color: #d0d0d0; /* 252 Grey82 */ +} +.cef-253 { + color: #dadada; /* 253 Grey85 */ +} +.cef-254 { + color: #e4e4e4; /* 254 Grey89 */ +} +.cef-255 { + color: #eeeeee; /* 255 Grey93 */ +} + +/* extended colors, background */ +.ceb-0 { + background-color: #000000; /* 000 Black */ +} +.ceb-1 { + background-color: #7f0000; /* 001 DarkRed */ +} +.ceb-2 { + background-color: #00cd00; /* 002 DarkGreen */ +} +.ceb-3 { + background-color: #fc7f00; /* 003 Orange */ +} +.ceb-4 { + background-color: #0000ee; /* 004 DarkBlue */ +} +.ceb-5 { + background-color: #cd00cd; /* 005 DarkMagenta */ +} +.ceb-6 { + background-color: #00cdcd; /* 006 DarkCyan */ +} +.ceb-7 { + background-color: #e5e5e5; /* 007 LightGrey */ +} +.ceb-8 { + background-color: #7f7f7f; /* 008 DarkGrey */ +} +.ceb-9 { + background-color: #ff0000; /* 009 LightRed */ +} +.ceb-10 { + background-color: #00ff00; /* 010 LightGreen */ +} +.ceb-11 { + background-color: #ffff00; /* 011 LightYellow */ +} +.ceb-12 { + background-color: #5c5cff; /* 012 LightBlue */ +} +.ceb-13 { + background-color: #ff00ff; /* 013 LightMagenta */ +} +.ceb-14 { + background-color: #00ffff; /* 014 LightCyan */ +} +.ceb-15 { + background-color: #ffffff; /* 015 White */ +} +.ceb-16 { + background-color: #000000; /* 016 Grey0 */ +} +.ceb-17 { + background-color: #00005f; /* 017 NavyBlue */ +} +.ceb-18 { + background-color: #000087; /* 018 DarkBlue */ +} +.ceb-19 { + background-color: #0000af; /* 019 Blue3 */ +} +.ceb-20 { + background-color: #0000d7; /* 020 Blue3 */ +} +.ceb-21 { + background-color: #0000ff; /* 021 Blue1 */ +} +.ceb-22 { + background-color: #005f00; /* 022 DarkGreen */ +} +.ceb-23 { + background-color: #005f5f; /* 023 DeepSkyBlue4 */ +} +.ceb-24 { + background-color: #005f87; /* 024 DeepSkyBlue4 */ +} +.ceb-25 { + background-color: #005faf; /* 025 DeepSkyBlue4 */ +} +.ceb-26 { + background-color: #005fd7; /* 026 DodgerBlue3 */ +} +.ceb-27 { + background-color: #005fff; /* 027 DodgerBlue2 */ +} +.ceb-28 { + background-color: #008700; /* 028 Green4 */ +} +.ceb-29 { + background-color: #00875f; /* 029 SpringGreen4 */ +} +.ceb-30 { + background-color: #008787; /* 030 Turquoise4 */ +} +.ceb-31 { + background-color: #0087af; /* 031 DeepSkyBlue3 */ +} +.ceb-32 { + background-color: #0087d7; /* 032 DeepSkyBlue3 */ +} +.ceb-33 { + background-color: #0087ff; /* 033 DodgerBlue1 */ +} +.ceb-34 { + background-color: #00af00; /* 034 Green3 */ +} +.ceb-35 { + background-color: #00af5f; /* 035 SpringGreen3 */ +} +.ceb-36 { + background-color: #00af87; /* 036 DarkCyan */ +} +.ceb-37 { + background-color: #00afaf; /* 037 LightSeaGreen */ +} +.ceb-38 { + background-color: #00afd7; /* 038 DeepSkyBlue2 */ +} +.ceb-39 { + background-color: #00afff; /* 039 DeepSkyBlue1 */ +} +.ceb-40 { + background-color: #00d700; /* 040 Green3 */ +} +.ceb-41 { + background-color: #00d75f; /* 041 SpringGreen3 */ +} +.ceb-42 { + background-color: #00d787; /* 042 SpringGreen2 */ +} +.ceb-43 { + background-color: #00d7af; /* 043 Cyan3 */ +} +.ceb-44 { + background-color: #00d7d7; /* 044 DarkTurquoise */ +} +.ceb-45 { + background-color: #00d7ff; /* 045 Turquoise2 */ +} +.ceb-46 { + background-color: #00ff00; /* 046 Green1 */ +} +.ceb-47 { + background-color: #00ff5f; /* 047 SpringGreen2 */ +} +.ceb-48 { + background-color: #00ff87; /* 048 SpringGreen1 */ +} +.ceb-49 { + background-color: #00ffaf; /* 049 MediumSpringGreen */ +} +.ceb-50 { + background-color: #00ffd7; /* 050 Cyan2 */ +} +.ceb-51 { + background-color: #00ffff; /* 051 Cyan1 */ +} +.ceb-52 { + background-color: #5f0000; /* 052 DarkRed */ +} +.ceb-53 { + background-color: #5f005f; /* 053 DeepPink4 */ +} +.ceb-54 { + background-color: #5f0087; /* 054 Purple4 */ +} +.ceb-55 { + background-color: #5f00af; /* 055 Purple4 */ +} +.ceb-56 { + background-color: #5f00d7; /* 056 Purple3 */ +} +.ceb-57 { + background-color: #5f00ff; /* 057 BlueViolet */ +} +.ceb-58 { + background-color: #5f5f00; /* 058 Orange4 */ +} +.ceb-59 { + background-color: #5f5f5f; /* 059 Grey37 */ +} +.ceb-60 { + background-color: #5f5f87; /* 060 MediumPurple4 */ +} +.ceb-61 { + background-color: #5f5faf; /* 061 SlateBlue3 */ +} +.ceb-62 { + background-color: #5f5fd7; /* 062 SlateBlue3 */ +} +.ceb-63 { + background-color: #5f5fff; /* 063 RoyalBlue1 */ +} +.ceb-64 { + background-color: #5f8700; /* 064 Chartreuse4 */ +} +.ceb-65 { + background-color: #5f875f; /* 065 DarkSeaGreen4 */ +} +.ceb-66 { + background-color: #5f8787; /* 066 PaleTurquoise4 */ +} +.ceb-67 { + background-color: #5f87af; /* 067 SteelBlue */ +} +.ceb-68 { + background-color: #5f87d7; /* 068 SteelBlue3 */ +} +.ceb-69 { + background-color: #5f87ff; /* 069 CornflowerBlue */ +} +.ceb-70 { + background-color: #5faf00; /* 070 Chartreuse3 */ +} +.ceb-71 { + background-color: #5faf5f; /* 071 DarkSeaGreen4 */ +} +.ceb-72 { + background-color: #5faf87; /* 072 CadetBlue */ +} +.ceb-73 { + background-color: #5fafaf; /* 073 CadetBlue */ +} +.ceb-74 { + background-color: #5fafd7; /* 074 SkyBlue3 */ +} +.ceb-75 { + background-color: #5fafff; /* 075 SteelBlue1 */ +} +.ceb-76 { + background-color: #5fd700; /* 076 Chartreuse3 */ +} +.ceb-77 { + background-color: #5fd75f; /* 077 PaleGreen3 */ +} +.ceb-78 { + background-color: #5fd787; /* 078 SeaGreen3 */ +} +.ceb-79 { + background-color: #5fd7af; /* 079 Aquamarine3 */ +} +.ceb-80 { + background-color: #5fd7d7; /* 080 MediumTurquoise */ +} +.ceb-81 { + background-color: #5fd7ff; /* 081 SteelBlue1 */ +} +.ceb-82 { + background-color: #5fff00; /* 082 Chartreuse2 */ +} +.ceb-83 { + background-color: #5fff5f; /* 083 SeaGreen2 */ +} +.ceb-84 { + background-color: #5fff87; /* 084 SeaGreen1 */ +} +.ceb-85 { + background-color: #5fffaf; /* 085 SeaGreen1 */ +} +.ceb-86 { + background-color: #5fffd7; /* 086 Aquamarine1 */ +} +.ceb-87 { + background-color: #5fffff; /* 087 DarkSlateGray2 */ +} +.ceb-88 { + background-color: #870000; /* 088 DarkRed */ +} +.ceb-89 { + background-color: #87005f; /* 089 DeepPink4 */ +} +.ceb-90 { + background-color: #870087; /* 090 DarkMagenta */ +} +.ceb-91 { + background-color: #8700af; /* 091 DarkMagenta */ +} +.ceb-92 { + background-color: #8700d7; /* 092 DarkViolet */ +} +.ceb-93 { + background-color: #8700ff; /* 093 Purple */ +} +.ceb-94 { + background-color: #875f00; /* 094 Orange4 */ +} +.ceb-95 { + background-color: #875f5f; /* 095 LightPink4 */ +} +.ceb-96 { + background-color: #875f87; /* 096 Plum4 */ +} +.ceb-97 { + background-color: #875faf; /* 097 MediumPurple3 */ +} +.ceb-98 { + background-color: #875fd7; /* 098 MediumPurple3 */ +} +.ceb-99 { + background-color: #875fff; /* 099 SlateBlue1 */ +} +.ceb-100 { + background-color: #878700; /* 100 Yellow4 */ +} +.ceb-101 { + background-color: #87875f; /* 101 Wheat4 */ +} +.ceb-102 { + background-color: #878787; /* 102 Grey53 */ +} +.ceb-103 { + background-color: #8787af; /* 103 LightSlateGrey */ +} +.ceb-104 { + background-color: #8787d7; /* 104 MediumPurple */ +} +.ceb-105 { + background-color: #8787ff; /* 105 LightSlateBlue */ +} +.ceb-106 { + background-color: #87af00; /* 106 Yellow4 */ +} +.ceb-107 { + background-color: #87af5f; /* 107 DarkOliveGreen3 */ +} +.ceb-108 { + background-color: #87af87; /* 108 DarkSeaGreen */ +} +.ceb-109 { + background-color: #87afaf; /* 109 LightSkyBlue3 */ +} +.ceb-110 { + background-color: #87afd7; /* 110 LightSkyBlue3 */ +} +.ceb-111 { + background-color: #87afff; /* 111 SkyBlue2 */ +} +.ceb-112 { + background-color: #87d700; /* 112 Chartreuse2 */ +} +.ceb-113 { + background-color: #87d75f; /* 113 DarkOliveGreen3 */ +} +.ceb-114 { + background-color: #87d787; /* 114 PaleGreen3 */ +} +.ceb-115 { + background-color: #87d7af; /* 115 DarkSeaGreen3 */ +} +.ceb-116 { + background-color: #87d7d7; /* 116 DarkSlateGray3 */ +} +.ceb-117 { + background-color: #87d7ff; /* 117 SkyBlue1 */ +} +.ceb-118 { + background-color: #87ff00; /* 118 Chartreuse1 */ +} +.ceb-119 { + background-color: #87ff5f; /* 119 LightGreen */ +} +.ceb-120 { + background-color: #87ff87; /* 120 LightGreen */ +} +.ceb-121 { + background-color: #87ffaf; /* 121 PaleGreen1 */ +} +.ceb-122 { + background-color: #87ffd7; /* 122 Aquamarine1 */ +} +.ceb-123 { + background-color: #87ffff; /* 123 DarkSlateGray1 */ +} +.ceb-124 { + background-color: #af0000; /* 124 Red3 */ +} +.ceb-125 { + background-color: #af005f; /* 125 DeepPink4 */ +} +.ceb-126 { + background-color: #af0087; /* 126 MediumVioletRed */ +} +.ceb-127 { + background-color: #af00af; /* 127 Magenta3 */ +} +.ceb-128 { + background-color: #af00d7; /* 128 DarkViolet */ +} +.ceb-129 { + background-color: #af00ff; /* 129 Purple */ +} +.ceb-130 { + background-color: #af5f00; /* 130 DarkOrange3 */ +} +.ceb-131 { + background-color: #af5f5f; /* 131 IndianRed */ +} +.ceb-132 { + background-color: #af5f87; /* 132 HotPink3 */ +} +.ceb-133 { + background-color: #af5faf; /* 133 MediumOrchid3 */ +} +.ceb-134 { + background-color: #af5fd7; /* 134 MediumOrchid */ +} +.ceb-135 { + background-color: #af5fff; /* 135 MediumPurple2 */ +} +.ceb-136 { + background-color: #af8700; /* 136 DarkGoldenrod */ +} +.ceb-137 { + background-color: #af875f; /* 137 LightSalmon3 */ +} +.ceb-138 { + background-color: #af8787; /* 138 RosyBrown */ +} +.ceb-139 { + background-color: #af87af; /* 139 Grey63 */ +} +.ceb-140 { + background-color: #af87d7; /* 140 MediumPurple2 */ +} +.ceb-141 { + background-color: #af87ff; /* 141 MediumPurple1 */ +} +.ceb-142 { + background-color: #afaf00; /* 142 Gold3 */ +} +.ceb-143 { + background-color: #afaf5f; /* 143 DarkKhaki */ +} +.ceb-144 { + background-color: #afaf87; /* 144 NavajoWhite3 */ +} +.ceb-145 { + background-color: #afafaf; /* 145 Grey69 */ +} +.ceb-146 { + background-color: #afafd7; /* 146 LightSteelBlue3 */ +} +.ceb-147 { + background-color: #afafff; /* 147 LightSteelBlue */ +} +.ceb-148 { + background-color: #afd700; /* 148 Yellow3 */ +} +.ceb-149 { + background-color: #afd75f; /* 149 DarkOliveGreen3 */ +} +.ceb-150 { + background-color: #afd787; /* 150 DarkSeaGreen3 */ +} +.ceb-151 { + background-color: #afd7af; /* 151 DarkSeaGreen2 */ +} +.ceb-152 { + background-color: #afd7d7; /* 152 LightCyan3 */ +} +.ceb-153 { + background-color: #afd7ff; /* 153 LightSkyBlue1 */ +} +.ceb-154 { + background-color: #afff00; /* 154 GreenYellow */ +} +.ceb-155 { + background-color: #afff5f; /* 155 DarkOliveGreen2 */ +} +.ceb-156 { + background-color: #afff87; /* 156 PaleGreen1 */ +} +.ceb-157 { + background-color: #afffaf; /* 157 DarkSeaGreen2 */ +} +.ceb-158 { + background-color: #afffd7; /* 158 DarkSeaGreen1 */ +} +.ceb-159 { + background-color: #afffff; /* 159 PaleTurquoise1 */ +} +.ceb-160 { + background-color: #d70000; /* 160 Red3 */ +} +.ceb-161 { + background-color: #d7005f; /* 161 DeepPink3 */ +} +.ceb-162 { + background-color: #d70087; /* 162 DeepPink3 */ +} +.ceb-163 { + background-color: #d700af; /* 163 Magenta3 */ +} +.ceb-164 { + background-color: #d700d7; /* 164 Magenta3 */ +} +.ceb-165 { + background-color: #d700ff; /* 165 Magenta2 */ +} +.ceb-166 { + background-color: #d75f00; /* 166 DarkOrange3 */ +} +.ceb-167 { + background-color: #d75f5f; /* 167 IndianRed */ +} +.ceb-168 { + background-color: #d75f87; /* 168 HotPink3 */ +} +.ceb-169 { + background-color: #d75faf; /* 169 HotPink2 */ +} +.ceb-170 { + background-color: #d75fd7; /* 170 Orchid */ +} +.ceb-171 { + background-color: #d75fff; /* 171 MediumOrchid1 */ +} +.ceb-172 { + background-color: #d78700; /* 172 Orange3 */ +} +.ceb-173 { + background-color: #d7875f; /* 173 LightSalmon3 */ +} +.ceb-174 { + background-color: #d78787; /* 174 LightPink3 */ +} +.ceb-175 { + background-color: #d787af; /* 175 Pink3 */ +} +.ceb-176 { + background-color: #d787d7; /* 176 Plum3 */ +} +.ceb-177 { + background-color: #d787ff; /* 177 Violet */ +} +.ceb-178 { + background-color: #d7af00; /* 178 Gold3 */ +} +.ceb-179 { + background-color: #d7af5f; /* 179 LightGoldenrod3 */ +} +.ceb-180 { + background-color: #d7af87; /* 180 Tan */ +} +.ceb-181 { + background-color: #d7afaf; /* 181 MistyRose3 */ +} +.ceb-182 { + background-color: #d7afd7; /* 182 Thistle3 */ +} +.ceb-183 { + background-color: #d7afff; /* 183 Plum2 */ +} +.ceb-184 { + background-color: #d7d700; /* 184 Yellow3 */ +} +.ceb-185 { + background-color: #d7d75f; /* 185 Khaki3 */ +} +.ceb-186 { + background-color: #d7d787; /* 186 LightGoldenrod2 */ +} +.ceb-187 { + background-color: #d7d7af; /* 187 LightYellow3 */ +} +.ceb-188 { + background-color: #d7d7d7; /* 188 Grey84 */ +} +.ceb-189 { + background-color: #d7d7ff; /* 189 LightSteelBlue1 */ +} +.ceb-190 { + background-color: #d7ff00; /* 190 Yellow2 */ +} +.ceb-191 { + background-color: #d7ff5f; /* 191 DarkOliveGreen1 */ +} +.ceb-192 { + background-color: #d7ff87; /* 192 DarkOliveGreen1 */ +} +.ceb-193 { + background-color: #d7ffaf; /* 193 DarkSeaGreen1 */ +} +.ceb-194 { + background-color: #d7ffd7; /* 194 Honeydew2 */ +} +.ceb-195 { + background-color: #d7ffff; /* 195 LightCyan1 */ +} +.ceb-196 { + background-color: #ff0000; /* 196 Red1 */ +} +.ceb-197 { + background-color: #ff005f; /* 197 DeepPink2 */ +} +.ceb-198 { + background-color: #ff0087; /* 198 DeepPink1 */ +} +.ceb-199 { + background-color: #ff00af; /* 199 DeepPink1 */ +} +.ceb-200 { + background-color: #ff00d7; /* 200 Magenta2 */ +} +.ceb-201 { + background-color: #ff00ff; /* 201 Magenta1 */ +} +.ceb-202 { + background-color: #ff5f00; /* 202 OrangeRed1 */ +} +.ceb-203 { + background-color: #ff5f5f; /* 203 IndianRed1 */ +} +.ceb-204 { + background-color: #ff5f87; /* 204 IndianRed1 */ +} +.ceb-205 { + background-color: #ff5faf; /* 205 HotPink */ +} +.ceb-206 { + background-color: #ff5fd7; /* 206 HotPink */ +} +.ceb-207 { + background-color: #ff5fff; /* 207 MediumOrchid1 */ +} +.ceb-208 { + background-color: #ff8700; /* 208 DarkOrange */ +} +.ceb-209 { + background-color: #ff875f; /* 209 Salmon1 */ +} +.ceb-210 { + background-color: #ff8787; /* 210 LightCoral */ +} +.ceb-211 { + background-color: #ff87af; /* 211 PaleVioletRed1 */ +} +.ceb-212 { + background-color: #ff87d7; /* 212 Orchid2 */ +} +.ceb-213 { + background-color: #ff87ff; /* 213 Orchid1 */ +} +.ceb-214 { + background-color: #ffaf00; /* 214 Orange1 */ +} +.ceb-215 { + background-color: #ffaf5f; /* 215 SandyBrown */ +} +.ceb-216 { + background-color: #ffaf87; /* 216 LightSalmon1 */ +} +.ceb-217 { + background-color: #ffafaf; /* 217 LightPink1 */ +} +.ceb-218 { + background-color: #ffafd7; /* 218 Pink1 */ +} +.ceb-219 { + background-color: #ffafff; /* 219 Plum1 */ +} +.ceb-220 { + background-color: #ffd700; /* 220 Gold1 */ +} +.ceb-221 { + background-color: #ffd75f; /* 221 LightGoldenrod2 */ +} +.ceb-222 { + background-color: #ffd787; /* 222 LightGoldenrod2 */ +} +.ceb-223 { + background-color: #ffd7af; /* 223 NavajoWhite1 */ +} +.ceb-224 { + background-color: #ffd7d7; /* 224 MistyRose1 */ +} +.ceb-225 { + background-color: #ffd7ff; /* 225 Thistle1 */ +} +.ceb-226 { + background-color: #ffff00; /* 226 Yellow1 */ +} +.ceb-227 { + background-color: #ffff5f; /* 227 LightGoldenrod1 */ +} +.ceb-228 { + background-color: #ffff87; /* 228 Khaki1 */ +} +.ceb-229 { + background-color: #ffffaf; /* 229 Wheat1 */ +} +.ceb-230 { + background-color: #ffffd7; /* 230 Cornsilk1 */ +} +.ceb-231 { + background-color: #ffffff; /* 231 Grey100 */ +} +.ceb-232 { + background-color: #080808; /* 232 Grey3 */ +} +.ceb-233 { + background-color: #121212; /* 233 Grey7 */ +} +.ceb-234 { + background-color: #1c1c1c; /* 234 Grey11 */ +} +.ceb-235 { + background-color: #262626; /* 235 Grey15 */ +} +.ceb-236 { + background-color: #303030; /* 236 Grey19 */ +} +.ceb-237 { + background-color: #3a3a3a; /* 237 Grey23 */ +} +.ceb-238 { + background-color: #444444; /* 238 Grey27 */ +} +.ceb-239 { + background-color: #4e4e4e; /* 239 Grey30 */ +} +.ceb-240 { + background-color: #585858; /* 240 Grey35 */ +} +.ceb-241 { + background-color: #626262; /* 241 Grey39 */ +} +.ceb-242 { + background-color: #6c6c6c; /* 242 Grey42 */ +} +.ceb-243 { + background-color: #767676; /* 243 Grey46 */ +} +.ceb-244 { + background-color: #808080; /* 244 Grey50 */ +} +.ceb-245 { + background-color: #8a8a8a; /* 245 Grey54 */ +} +.ceb-246 { + background-color: #949494; /* 246 Grey58 */ +} +.ceb-247 { + background-color: #9e9e9e; /* 247 Grey62 */ +} +.ceb-248 { + background-color: #a8a8a8; /* 248 Grey66 */ +} +.ceb-249 { + background-color: #b2b2b2; /* 249 Grey70 */ +} +.ceb-250 { + background-color: #bcbcbc; /* 250 Grey74 */ +} +.ceb-251 { + background-color: #c6c6c6; /* 251 Grey78 */ +} +.ceb-252 { + background-color: #d0d0d0; /* 252 Grey82 */ +} +.ceb-253 { + background-color: #dadada; /* 253 Grey85 */ +} +.ceb-254 { + background-color: #e4e4e4; /* 254 Grey89 */ +} +.ceb-255 { + background-color: #eeeeee; /* 255 Grey93 */ +} + +/* attributes overrides */ +.a-b { + font-weight: bold; +} +.a-no-b { + font-weight: normal; +} +.a-i { + font-style: italic; +} +.a-no-i { + font-style: normal; +} +.a-u { + text-decoration: underline; +} +.a-no-u { + text-decoration: none; +} +.a-reset { + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +/* highlight messages */ +.highlight { + color: yellow; + font-weight: bold; +} + +/* */ +/* Mobile layout */ +/* */ +@media (max-width: 968px) { + /* a different colour is too irregular on mobile */ + .repeated-time .cof-chat_time, + .repeated-time .cof-chat_time_delimiters { + color: #999; + } + + #nicklist { + -webkit-box-shadow: 0px 0px 120px #000; + box-shadow: 0px 0px 120px #000; + background: #282828; + } + + .footer { + background: rgb(24,24,24); + } + +} + + + + diff --git a/sources/css/themes/light.css b/sources/css/themes/light.css new file mode 100644 index 0000000..6a5c109 --- /dev/null +++ b/sources/css/themes/light.css @@ -0,0 +1,2102 @@ +body { + background-color: #fdfdfd; + color: #181818; +} +#nicklist, .footer { + background-color: #f7f7f7; +} + +html { + background-color: inherit; +} + +.repeated-time .cob-chat_time, +.repeated-time .cob-chat_time_delimiters { + background-color: transparent; + color: #ccc; +} + +/* fix for mobile firefox which ignores :hover */ +.nav-pills > li > a:active, .nav-pills > li > a:active span { + color: #eee; + background-color: #222; +} + +.btn-send, +.btn-send-image, { + background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.3); + color: #428BCA; +} + +tr.bufferline:hover { + background-color: #efefef; +} + +.danger, .alert-danger, .badge .alert-danger, .badge.danger { + background-color: rgb(217, 83, 79); + color: #ddd; +} +.alert-danger { + border-color: #121212; + color: black; +} + +li.notification { + color: green; +} + +::-webkit-scrollbar-track-piece { + background-color: black; +} +::-webkit-scrollbar-thumb:vertical { + height: 15px; + background: rgba(255,255,255,0.5); +} + +.gb-modal .backdrop { + background-color:rgba(255, 255, 255, 0.5) +} + +select.form-control, select option, input[type=text], input[type=password], #sendMessage, .badge { + color: #333; + box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1), 0px 1px 7px 0px rgba(255, 255, 255, 0.8) inset; + background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.3); +} + +#connection-infos { + color: #aaa; +} + +.nav-pills li:nth-child(2n) { + background: #e1e1e1; +} + +.nav-pills > li > a { + color: #222; +} + +.nav-pills > li > a:hover, .nav-pills > li > a:hover span { + color: #555; +} + +.color-light-green { + color: chartreuse; +} + +.color-27 { + color: deepskyblue; +} + +#topbar .actions { + background: #e7e7e7; +} + +#topbar, #sidebar, .panel, .dropdown-menu, .modal-content { + background: #e7e7e7; +} + +#sidebar .buffer .buffer-quick-key { + text-shadow: -1px 0px 4px rgba(0, 0, 0, 0.2), + 0px -1px 4px rgba(0, 0, 0, 0.2), + 1px 0px 4px rgba(0, 0, 0, 0.2), + 0px 1px 4px rgba(0, 0, 0, 0.2); +} + +#nicklist a:hover { + background: #eee; +} + +.horizontal-line { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; + box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; + border-bottom: 1px solid #121212; +} +.vertical-line { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; + box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; + border-right: 1px solid #121212; +} +.vertical-line-left { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; + box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; + border-left: 1px solid #121212; +} +.panel-group .panel-heading + .panel-collapse .panel-body, .modal-body, .modal-header, .modal-footer { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; + box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; + border-top: 1px solid #121212; +} + +#readmarker { + border-top-color: rgba(255, 255, 255, 0.3); + border-bottom-color: #121212; +} + +/****************************/ +/* Weechat colors and style */ +/****************************/ +/* style options, foreground */ +.cof-separator { + color: #345A6A; +} +.cof-chat { + color: #575757; +} +.cof-chat_time { + color: #636363; +} +.cof-chat_time_delimiters { + color: #66421E; +} +.cof-chat_prefix_error { + color: #80723C; +} +.cof-chat_prefix_network { + color: #644764; +} +.cof-chat_prefix_action { + color: #636363; +} +.cof-chat_prefix_join { + color: #525F2A; +} +.cof-chat_prefix_quit { + color: #642B23; +} +.cof-chat_prefix_more { + color: #644764; +} +.cof-chat_prefix_suffix { + color: #525F2A; +} +.cof-chat_buffer { + color: #4A4A4A; +} +.cof-chat_server { + color: #523518; +} +.cof-chat_channel { + color: #636363; +} +.cof-chat_nick { + color: #305956; +} +.cof-chat_nick_self { + color: #4C4C4C; + font-weight: bold; +} +.cof-chat_nick_other { + color: #77dfd8; +} +.cof-invalid { + /* should never happen */ + color: #4A4A4A; + background-color: transparent; +} +.cof-chat_host { + color: #305956; +} +.cof-chat_delimiters { + color: #525F2A; +} +.cof-chat_highlight { + color: #665B30; + background-color: #504054; +} +.cof-chat_read_marker { + color: #644764; +} +.cof-chat_text_found { + color: #665B30; +} +.cof-chat_value { + color: #305956; +} +.cof-chat_prefix_buffer { + color: #523518; +} +.cof-chat_tags { + color: #50221C; +} +.cof-chat_inactive_window { + color: #2E2E2E; +} +.cof-chat_inactive_buffer { + color: #2E2E2E; +} +.cof-chat_prefix_buffer_inactive_buffer { + color: #2E2E2E; +} +.cof-chat_nick_offline { + color: #2E2E2E; +} +.cof-chat_nick_offline_highlight { + color: #636363; + background-color: #5d5d5d; +} +.cof-chat_nick_prefix { + color: #525F2A; +} +.cof-chat_nick_suffix { + color: #525F2A; +} +.cof-emphasis { + color: #80723C; + background-color: #645068; +} +.cof-chat_day_change { + color: #305956; +} + +/* style options, background */ +.cob-separator { +} +.cob-chat { +} +.cob-chat_time { + color: #999; +} +.cob-chat_time_delimiters { +} +.cob-chat_prefix_error { +} +.cob-chat_prefix_network { +} +.cob-chat_prefix_action { +} +.cob-chat_prefix_join { +} +.cob-chat_prefix_quit { +} +.cob-chat_prefix_more { +} +.cob-chat_prefix_suffix { +} +.cob-chat_buffer { +} +.cob-chat_server { +} +.cob-chat_channel { +} +.cob-chat_nick { +} +.cob-chat_nick_self { +} +.cob-chat_nick_other { +} +.cob-invalid { +} +.cob-chat_host { +} +.cob-chat_delimiters { +} +.cob-chat_highlight { + background-color: #c8a0d1; +} +.cob-chat_read_marker { +} +.cob-chat_text_found { +} +.cob-chat_value { +} +.cob-chat_prefix_buffer { +} +.cob-chat_tags { +} +.cob-chat_inactive_window { +} +.cob-chat_inactive_buffer { +} +.cob-chat_prefix_buffer_inactive_buffer { +} +.cob-chat_nick_offline { +} +.cob-chat_nick_offline_highlight { + background-color: #5d5d5d; +} +.cob-chat_nick_prefix { +} +.cob-chat_nick_suffix { +} +.cob-emphasis { + background-color: #c8a0d1; +} +.cob-chat_day_change { +} + +/* style options, attributes */ +.coa-separator { +} +.coa-chat { +} +.coa-chat_time { +} +.coa-chat_time_delimiters { +} +.coa-chat_prefix_error { +} +.coa-chat_prefix_network { +} +.coa-chat_prefix_action { +} +.coa-chat_prefix_join { +} +.coa-chat_prefix_quit { +} +.coa-chat_prefix_more { +} +.coa-chat_prefix_suffix { +} +.coa-chat_buffer { +} +.coa-chat_server { +} +.coa-chat_channel { +} +.coa-chat_nick { +} +.coa-chat_nick_self { +} +.coa-chat_nick_other { +} +.coa-invalid { +} +.coa-chat_host { +} +.coa-chat_delimiters { +} +.coa-chat_highlight { +} +.coa-chat_read_marker { +} +.coa-chat_text_found { +} +.coa-chat_value { +} +.coa-chat_prefix_buffer { +} +.coa-chat_tags { +} +.coa-chat_inactive_window { +} +.coa-chat_inactive_buffer { +} +.coa-chat_prefix_buffer_inactive_buffer { +} +.coa-chat_nick_offline { +} +.coa-chat_nick_offline_highlight { +} +.coa-chat_nick_prefix { +} +.coa-chat_nick_suffix { +} +.coa-emphasis { +} +.coa-chat_day_change { +} + +/* WeeChat colors, foreground */ +.cwf-default { + color: #282828; +} + +.cwf-black { + color: #000000; +} +.cwf-darkgray { + color: #2E2E2E; +} +.cwf-red { + color: #642B23; +} +.cwf-lightred { + color: #704B48; +} +.cwf-green { + color: #475A1E; +} +.cwf-lightgreen { + color: #667734; +} +.cwf-brown { + color: #593919; +} +.cwf-yellow { + color: #80723C; +} +.cwf-blue { + color: #395A66; +} +.cwf-lightblue { + color: #4E6C78; +} +.cwf-magenta { + color: #504054; +} +.cwf-lightmagenta { + color: #644764; +} +.cwf-cyan { + color: #10434A; +} +.cwf-lightcyan { + color: #182D2B; +} +.cwf-gray { + color: #464646; +} +.cwf-white { + color: #4A4A4A; +} + +/* WeeChat colors, background */ +.cwb-default { + background-color: transparent; +} +.cwb-black { + background-color: #000000; +} +.cwb-darkgray { + background-color: #2E2E2E; +} +.cwb-red { + background-color: #642B23; +} +.cwb-lightred { + background-color: #704B48; +} +.cwb-green { + background-color: #475A1E; +} +.cwb-lightgreen { + background-color: #667734; +} +.cwb-brown { + background-color: #593919; +} +.cwb-yellow { + background-color: #80723C; +} +.cwb-blue { + background-color: #395A66; +} +.cwb-lightblue { + background-color: #4E6C78; +} +.cwb-magenta { + background-color: #504054; +} +.cwb-lightmagenta { + background-color: #644764; +} +.cwb-cyan { + background-color: #10434A; +} +.cwb-lightcyan { + background-color: #182D2B; +} +.cwb-gray { + background-color: #464646; +} +.cwb-white { + background-color: #4A4A4A; +} + +/* extended colors, foreground */ +.cef-0 { + color: #000000; /* 000 Black */ +} +.cef-1 { + color: #7f0000; /* 001 DarkRed */ +} +.cef-2 { + color: #009300; /* 002 DarkGreen */ +} +.cef-3 { + color: #fc7f00; /* 003 Orange */ +} +.cef-4 { + color: #0000ee; /* 004 DarkBlue */ +} +.cef-5 { + color: #8e00cd; /* 005 DarkMagenta */ +} +.cef-6 { + color: #009393; /* 006 DarkCyan */ +} +.cef-7 { + color: #848484; /* 007 LightGrey */ +} +.cef-8 { + color: #606060; /* 008 DarkGrey */ +} +.cef-9 { + color: #ff0000; /* 009 LightRed */ +} +.cef-10 { + color: #00ff00; /* 010 LightGreen */ +} +.cef-11 { + color: #d4d420; /* 011 LightYellow*/ +} +.cef-12 { + color: #4E80ff; /* 012 LightBlue */ +} +.cef-13 { + color: #e407e4; /* 013 LightMagenta */ +} +.cef-14 { + color: #18e1e1; /* 014 LightCyan */ +} +.cef-15 { + color: #4A4A4A; /* 015 White */ +} +.cef-16 { + color: #000000; /* 016 Grey0 */ +} +.cef-17 { + color: #00005f; /* 017 NavyBlue */ +} +.cef-18 { + color: #000087; /* 018 DarkBlue */ +} +.cef-19 { + color: #0000af; /* 019 Blue3 */ +} +.cef-20 { + color: #0000d7; /* 020 Blue3 */ +} +.cef-21 { + color: #0000ff; /* 021 Blue1 */ +} +.cef-22 { + color: #005f00; /* 022 DarkGreen */ +} +.cef-23 { + color: #005f5f; /* 023 DeepSkyBlue4 */ +} +.cef-24 { + color: #005f87; /* 024 DeepSkyBlue4 */ +} +.cef-25 { + color: #005faf; /* 025 DeepSkyBlue4 */ +} +.cef-26 { + color: #005fd7; /* 026 DodgerBlue3 */ +} +.cef-27 { + color: #005fff; /* 027 DodgerBlue2 */ +} +.cef-28 { + color: #008700; /* 028 Green4 */ +} +.cef-29 { + color: #00875f; /* 029 SpringGreen4 */ +} +.cef-30 { + color: #008787; /* 030 Turquoise4 */ +} +.cef-31 { + color: #0087af; /* 031 DeepSkyBlue3 */ +} +.cef-32 { + color: #0087d7; /* 032 DeepSkyBlue3 */ +} +.cef-33 { + color: #0087ff; /* 033 DodgerBlue1 */ +} +.cef-34 { + color: #00af00; /* 034 Green3 */ +} +.cef-35 { + color: #00af5f; /* 035 SpringGreen3 */ +} +.cef-36 { + color: #00af87; /* 036 DarkCyan */ +} +.cef-37 { + color: #00afaf; /* 037 LightSeaGreen */ +} +.cef-38 { + color: #00afd7; /* 038 DeepSkyBlue2 */ +} +.cef-39 { + color: #00afff; /* 039 DeepSkyBlue1 */ +} +.cef-40 { + color: #00d700; /* 040 Green3 */ +} +.cef-41 { + color: #00d75f; /* 041 SpringGreen3 */ +} +.cef-42 { + color: #00d787; /* 042 SpringGreen2 */ +} +.cef-43 { + color: #00d7af; /* 043 Cyan3 */ +} +.cef-44 { + color: #00d7d7; /* 044 DarkTurquoise */ +} +.cef-45 { + color: #00d7ff; /* 045 Turquoise2 */ +} +.cef-46 { + color: #00ff00; /* 046 Green1 */ +} +.cef-47 { + color: #00ff5f; /* 047 SpringGreen2 */ +} +.cef-48 { + color: #00ff87; /* 048 SpringGreen1 */ +} +.cef-49 { + color: #00ffaf; /* 049 MediumSpringGreen */ +} +.cef-50 { + color: #00ffd7; /* 050 Cyan2 */ +} +.cef-51 { + color: #00ffff; /* 051 Cyan1 */ +} +.cef-52 { + color: #5f0000; /* 052 DarkRed */ +} +.cef-53 { + color: #5f005f; /* 053 DeepPink4 */ +} +.cef-54 { + color: #5f0087; /* 054 Purple4 */ +} +.cef-55 { + color: #5f00af; /* 055 Purple4 */ +} +.cef-56 { + color: #5f00d7; /* 056 Purple3 */ +} +.cef-57 { + color: #5f00ff; /* 057 BlueViolet */ +} +.cef-58 { + color: #5f5f00; /* 058 Orange4 */ +} +.cef-59 { + color: #5f5f5f; /* 059 Grey37 */ +} +.cef-60 { + color: #5f5f87; /* 060 MediumPurple4 */ +} +.cef-61 { + color: #5f5faf; /* 061 SlateBlue3 */ +} +.cef-62 { + color: #5f5fd7; /* 062 SlateBlue3 */ +} +.cef-63 { + color: #5f5fff; /* 063 RoyalBlue1 */ +} +.cef-64 { + color: #5f8700; /* 064 Chartreuse4 */ +} +.cef-65 { + color: #5f875f; /* 065 DarkSeaGreen4 */ +} +.cef-66 { + color: #5f8787; /* 066 PaleTurquoise4 */ +} +.cef-67 { + color: #5f87af; /* 067 SteelBlue */ +} +.cef-68 { + color: #5f87d7; /* 068 SteelBlue3 */ +} +.cef-69 { + color: #5f87ff; /* 069 CornflowerBlue */ +} +.cef-70 { + color: #5faf00; /* 070 Chartreuse3 */ +} +.cef-71 { + color: #5faf5f; /* 071 DarkSeaGreen4 */ +} +.cef-72 { + color: #5faf87; /* 072 CadetBlue */ +} +.cef-73 { + color: #5fafaf; /* 073 CadetBlue */ +} +.cef-74 { + color: #5fafd7; /* 074 SkyBlue3 */ +} +.cef-75 { + color: #5fafff; /* 075 SteelBlue1 */ +} +.cef-76 { + color: #5fd700; /* 076 Chartreuse3 */ +} +.cef-77 { + color: #5fd75f; /* 077 PaleGreen3 */ +} +.cef-78 { + color: #5fd787; /* 078 SeaGreen3 */ +} +.cef-79 { + color: #5fd7af; /* 079 Aquamarine3 */ +} +.cef-80 { + color: #5fd7d7; /* 080 MediumTurquoise */ +} +.cef-81 { + color: #5fd7ff; /* 081 SteelBlue1 */ +} +.cef-82 { + color: #5fff00; /* 082 Chartreuse2 */ +} +.cef-83 { + color: #5fff5f; /* 083 SeaGreen2 */ +} +.cef-84 { + color: #5fff87; /* 084 SeaGreen1 */ +} +.cef-85 { + color: #5fffaf; /* 085 SeaGreen1 */ +} +.cef-86 { + color: #5fffd7; /* 086 Aquamarine1 */ +} +.cef-87 { + color: #5fffff; /* 087 DarkSlateGray2 */ +} +.cef-88 { + color: #870000; /* 088 DarkRed */ +} +.cef-89 { + color: #87005f; /* 089 DeepPink4 */ +} +.cef-90 { + color: #870087; /* 090 DarkMagenta */ +} +.cef-91 { + color: #8700af; /* 091 DarkMagenta */ +} +.cef-92 { + color: #8700d7; /* 092 DarkViolet */ +} +.cef-93 { + color: #8700ff; /* 093 Purple */ +} +.cef-94 { + color: #875f00; /* 094 Orange4 */ +} +.cef-95 { + color: #875f5f; /* 095 LightPink4 */ +} +.cef-96 { + color: #875f87; /* 096 Plum4 */ +} +.cef-97 { + color: #875faf; /* 097 MediumPurple3 */ +} +.cef-98 { + color: #875fd7; /* 098 MediumPurple3 */ +} +.cef-99 { + color: #875fff; /* 099 SlateBlue1 */ +} +.cef-100 { + color: #878700; /* 100 Yellow4 */ +} +.cef-101 { + color: #87875f; /* 101 Wheat4 */ +} +.cef-102 { + color: #878787; /* 102 Grey53 */ +} +.cef-103 { + color: #8787af; /* 103 LightSlateGrey */ +} +.cef-104 { + color: #8787d7; /* 104 MediumPurple */ +} +.cef-105 { + color: #8787ff; /* 105 LightSlateBlue */ +} +.cef-106 { + color: #87af00; /* 106 Yellow4 */ +} +.cef-107 { + color: #87af5f; /* 107 DarkOliveGreen3 */ +} +.cef-108 { + color: #87af87; /* 108 DarkSeaGreen */ +} +.cef-109 { + color: #87afaf; /* 109 LightSkyBlue3 */ +} +.cef-110 { + color: #87afd7; /* 110 LightSkyBlue3 */ +} +.cef-111 { + color: #87afff; /* 111 SkyBlue2 */ +} +.cef-112 { + color: #87d700; /* 112 Chartreuse2 */ +} +.cef-113 { + color: #87d75f; /* 113 DarkOliveGreen3 */ +} +.cef-114 { + color: #87d787; /* 114 PaleGreen3 */ +} +.cef-115 { + color: #87d7af; /* 115 DarkSeaGreen3 */ +} +.cef-116 { + color: #87d7d7; /* 116 DarkSlateGray3 */ +} +.cef-117 { + color: #87d7ff; /* 117 SkyBlue1 */ +} +.cef-118 { + color: #87ff00; /* 118 Chartreuse1 */ +} +.cef-119 { + color: #87ff5f; /* 119 LightGreen */ +} +.cef-120 { + color: #87ff87; /* 120 LightGreen */ +} +.cef-121 { + color: #87ffaf; /* 121 PaleGreen1 */ +} +.cef-122 { + color: #87ffd7; /* 122 Aquamarine1 */ +} +.cef-123 { + color: #87ffff; /* 123 DarkSlateGray1 */ +} +.cef-124 { + color: #af0000; /* 124 Red3 */ +} +.cef-125 { + color: #af005f; /* 125 DeepPink4 */ +} +.cef-126 { + color: #af0087; /* 126 MediumVioletRed */ +} +.cef-127 { + color: #af00af; /* 127 Magenta3 */ +} +.cef-128 { + color: #af00d7; /* 128 DarkViolet */ +} +.cef-129 { + color: #af00ff; /* 129 Purple */ +} +.cef-130 { + color: #af5f00; /* 130 DarkOrange3 */ +} +.cef-131 { + color: #af5f5f; /* 131 IndianRed */ +} +.cef-132 { + color: #af5f87; /* 132 HotPink3 */ +} +.cef-133 { + color: #af5faf; /* 133 MediumOrchid3 */ +} +.cef-134 { + color: #af5fd7; /* 134 MediumOrchid */ +} +.cef-135 { + color: #af5fff; /* 135 MediumPurple2 */ +} +.cef-136 { + color: #af8700; /* 136 DarkGoldenrod */ +} +.cef-137 { + color: #af875f; /* 137 LightSalmon3 */ +} +.cef-138 { + color: #af8787; /* 138 RosyBrown */ +} +.cef-139 { + color: #af87af; /* 139 Grey63 */ +} +.cef-140 { + color: #af87d7; /* 140 MediumPurple2 */ +} +.cef-141 { + color: #af87ff; /* 141 MediumPurple1 */ +} +.cef-142 { + color: #afaf00; /* 142 Gold3 */ +} +.cef-143 { + color: #afaf5f; /* 143 DarkKhaki */ +} +.cef-144 { + color: #afaf87; /* 144 NavajoWhite3 */ +} +.cef-145 { + color: #afafaf; /* 145 Grey69 */ +} +.cef-146 { + color: #afafd7; /* 146 LightSteelBlue3 */ +} +.cef-147 { + color: #afafff; /* 147 LightSteelBlue */ +} +.cef-148 { + color: #afd700; /* 148 Yellow3 */ +} +.cef-149 { + color: #afd75f; /* 149 DarkOliveGreen3 */ +} +.cef-150 { + color: #afd787; /* 150 DarkSeaGreen3 */ +} +.cef-151 { + color: #afd7af; /* 151 DarkSeaGreen2 */ +} +.cef-152 { + color: #afd7d7; /* 152 LightCyan3 */ +} +.cef-153 { + color: #afd7ff; /* 153 LightSkyBlue1 */ +} +.cef-154 { + color: #afff00; /* 154 GreenYellow */ +} +.cef-155 { + color: #afff5f; /* 155 DarkOliveGreen2 */ +} +.cef-156 { + color: #afff87; /* 156 PaleGreen1 */ +} +.cef-157 { + color: #afffaf; /* 157 DarkSeaGreen2 */ +} +.cef-158 { + color: #afffd7; /* 158 DarkSeaGreen1 */ +} +.cef-159 { + color: #afffff; /* 159 PaleTurquoise1 */ +} +.cef-160 { + color: #d70000; /* 160 Red3 */ +} +.cef-161 { + color: #d7005f; /* 161 DeepPink3 */ +} +.cef-162 { + color: #d70087; /* 162 DeepPink3 */ +} +.cef-163 { + color: #d700af; /* 163 Magenta3 */ +} +.cef-164 { + color: #d700d7; /* 164 Magenta3 */ +} +.cef-165 { + color: #d700ff; /* 165 Magenta2 */ +} +.cef-166 { + color: #d75f00; /* 166 DarkOrange3 */ +} +.cef-167 { + color: #d75f5f; /* 167 IndianRed */ +} +.cef-168 { + color: #d75f87; /* 168 HotPink3 */ +} +.cef-169 { + color: #d75faf; /* 169 HotPink2 */ +} +.cef-170 { + color: #d75fd7; /* 170 Orchid */ +} +.cef-171 { + color: #d75fff; /* 171 MediumOrchid1 */ +} +.cef-172 { + color: #d78700; /* 172 Orange3 */ +} +.cef-173 { + color: #d7875f; /* 173 LightSalmon3 */ +} +.cef-174 { + color: #d78787; /* 174 LightPink3 */ +} +.cef-175 { + color: #d787af; /* 175 Pink3 */ +} +.cef-176 { + color: #d787d7; /* 176 Plum3 */ +} +.cef-177 { + color: #d787ff; /* 177 Violet */ +} +.cef-178 { + color: #d7af00; /* 178 Gold3 */ +} +.cef-179 { + color: #d7af5f; /* 179 LightGoldenrod3 */ +} +.cef-180 { + color: #d7af87; /* 180 Tan */ +} +.cef-181 { + color: #d7afaf; /* 181 MistyRose3 */ +} +.cef-182 { + color: #d7afd7; /* 182 Thistle3 */ +} +.cef-183 { + color: #d7afff; /* 183 Plum2 */ +} +.cef-184 { + color: #d7d700; /* 184 Yellow3 */ +} +.cef-185 { + color: #d7d75f; /* 185 Khaki3 */ +} +.cef-186 { + color: #d7d787; /* 186 LightGoldenrod2 */ +} +.cef-187 { + color: #d7d7af; /* 187 LightYellow3 */ +} +.cef-188 { + color: #d7d7d7; /* 188 Grey84 */ +} +.cef-189 { + color: #d7d7ff; /* 189 LightSteelBlue1 */ +} +.cef-190 { + color: #d7ff00; /* 190 Yellow2 */ +} +.cef-191 { + color: #d7ff5f; /* 191 DarkOliveGreen1 */ +} +.cef-192 { + color: #d7ff87; /* 192 DarkOliveGreen1 */ +} +.cef-193 { + color: #d7ffaf; /* 193 DarkSeaGreen1 */ +} +.cef-194 { + color: #d7ffd7; /* 194 Honeydew2 */ +} +.cef-195 { + color: #d7ffff; /* 195 LightCyan1 */ +} +.cef-196 { + color: #ff0000; /* 196 Red1 */ +} +.cef-197 { + color: #ff005f; /* 197 DeepPink2 */ +} +.cef-198 { + color: #ff0087; /* 198 DeepPink1 */ +} +.cef-199 { + color: #ff00af; /* 199 DeepPink1 */ +} +.cef-200 { + color: #ff00d7; /* 200 Magenta2 */ +} +.cef-201 { + color: #ff00ff; /* 201 Magenta1 */ +} +.cef-202 { + color: #ff5f00; /* 202 OrangeRed1 */ +} +.cef-203 { + color: #ff5f5f; /* 203 IndianRed1 */ +} +.cef-204 { + color: #ff5f87; /* 204 IndianRed1 */ +} +.cef-205 { + color: #ff5faf; /* 205 HotPink */ +} +.cef-206 { + color: #ff5fd7; /* 206 HotPink */ +} +.cef-207 { + color: #ff5fff; /* 207 MediumOrchid1 */ +} +.cef-208 { + color: #ff8700; /* 208 DarkOrange */ +} +.cef-209 { + color: #ff875f; /* 209 Salmon1 */ +} +.cef-210 { + color: #ff8787; /* 210 LightCoral */ +} +.cef-211 { + color: #ff87af; /* 211 PaleVioletRed1 */ +} +.cef-212 { + color: #ff87d7; /* 212 Orchid2 */ +} +.cef-213 { + color: #ff87ff; /* 213 Orchid1 */ +} +.cef-214 { + color: #ffaf00; /* 214 Orange1 */ +} +.cef-215 { + color: #ffaf5f; /* 215 SandyBrown */ +} +.cef-216 { + color: #ffaf87; /* 216 LightSalmon1 */ +} +.cef-217 { + color: #ffafaf; /* 217 LightPink1 */ +} +.cef-218 { + color: #ffafd7; /* 218 Pink1 */ +} +.cef-219 { + color: #ffafff; /* 219 Plum1 */ +} +.cef-220 { + color: #ffd700; /* 220 Gold1 */ +} +.cef-221 { + color: #ffd75f; /* 221 LightGoldenrod2 */ +} +.cef-222 { + color: #ffd787; /* 222 LightGoldenrod2 */ +} +.cef-223 { + color: #ffd7af; /* 223 NavajoWhite1 */ +} +.cef-224 { + color: #ffd7d7; /* 224 MistyRose1 */ +} +.cef-225 { + color: #ffd7ff; /* 225 Thistle1 */ +} +.cef-226 { + color: #ffff00; /* 226 Yellow1 */ +} +.cef-227 { + color: #ffff5f; /* 227 LightGoldenrod1 */ +} +.cef-228 { + color: #ffff87; /* 228 Khaki1 */ +} +.cef-229 { + color: #ffffaf; /* 229 Wheat1 */ +} +.cef-230 { + color: #ffffd7; /* 230 Cornsilk1 */ +} +.cef-231 { + color: #ffffff; /* 231 Grey100 */ +} +.cef-232 { + color: #080808; /* 232 Grey3 */ +} +.cef-233 { + color: #121212; /* 233 Grey7 */ +} +.cef-234 { + color: #1c1c1c; /* 234 Grey11 */ +} +.cef-235 { + color: #262626; /* 235 Grey15 */ +} +.cef-236 { + color: #303030; /* 236 Grey19 */ +} +.cef-237 { + color: #3a3a3a; /* 237 Grey23 */ +} +.cef-238 { + color: #444444; /* 238 Grey27 */ +} +.cef-239 { + color: #4e4e4e; /* 239 Grey30 */ +} +.cef-240 { + color: #585858; /* 240 Grey35 */ +} +.cef-241 { + color: #626262; /* 241 Grey39 */ +} +.cef-242 { + color: #6c6c6c; /* 242 Grey42 */ +} +.cef-243 { + color: #767676; /* 243 Grey46 */ +} +.cef-244 { + color: #808080; /* 244 Grey50 */ +} +.cef-245 { + color: #8a8a8a; /* 245 Grey54 */ +} +.cef-246 { + color: #949494; /* 246 Grey58 */ +} +.cef-247 { + color: #9e9e9e; /* 247 Grey62 */ +} +.cef-248 { + color: #a8a8a8; /* 248 Grey66 */ +} +.cef-249 { + color: #b2b2b2; /* 249 Grey70 */ +} +.cef-250 { + color: #bcbcbc; /* 250 Grey74 */ +} +.cef-251 { + color: #c6c6c6; /* 251 Grey78 */ +} +.cef-252 { + color: #d0d0d0; /* 252 Grey82 */ +} +.cef-253 { + color: #dadada; /* 253 Grey85 */ +} +.cef-254 { + color: #e4e4e4; /* 254 Grey89 */ +} +.cef-255 { + color: #eeeeee; /* 255 Grey93 */ +} + +/* extended colors, background */ +.ceb-0 { + background-color: #000000; /* 000 Black */ +} +.ceb-1 { + background-color: #7f0000; /* 001 DarkRed */ +} +.ceb-2 { + background-color: #009300; /* 002 DarkGreen */ +} +.ceb-3 { + background-color: #fc7f00; /* 003 Orange */ +} +.ceb-4 { + background-color: #0000ee; /* 004 DarkBlue */ +} +.ceb-5 { + background-color: #8e00cd; /* 005 DarkMagenta */ +} +.ceb-6 { + background-color: #009393; /* 006 DarkCyan */ +} +.ceb-7 { + background-color: #e5e5e5; /* 007 LightGrey */ +} +.ceb-8 { + background-color: #7f7f7f; /* 008 DarkGrey */ +} +.ceb-9 { + background-color: #ff0000; /* 009 LightRed */ +} +.ceb-10 { + background-color: #00ff00; /* 010 LightGreen */ +} +.ceb-11 { + background-color: #ffff00; /* 011 LightYellow */ +} +.ceb-12 { + background-color: #0080ff; /* 012 LightBlue */ +} +.ceb-13 { + background-color: #ff00ff; /* 013 LightMagenta */ +} +.ceb-14 { + background-color: #00ffff; /* 014 LightCyan */ +} +.ceb-15 { + background-color: #ffffff; /* 015 White */ +} +.ceb-16 { + background-color: #000000; /* 016 Grey0 */ +} +.ceb-17 { + background-color: #00005f; /* 017 NavyBlue */ +} +.ceb-18 { + background-color: #000087; /* 018 DarkBlue */ +} +.ceb-19 { + background-color: #0000af; /* 019 Blue3 */ +} +.ceb-20 { + background-color: #0000d7; /* 020 Blue3 */ +} +.ceb-21 { + background-color: #0000ff; /* 021 Blue1 */ +} +.ceb-22 { + background-color: #005f00; /* 022 DarkGreen */ +} +.ceb-23 { + background-color: #005f5f; /* 023 DeepSkyBlue4 */ +} +.ceb-24 { + background-color: #005f87; /* 024 DeepSkyBlue4 */ +} +.ceb-25 { + background-color: #005faf; /* 025 DeepSkyBlue4 */ +} +.ceb-26 { + background-color: #005fd7; /* 026 DodgerBlue3 */ +} +.ceb-27 { + background-color: #005fff; /* 027 DodgerBlue2 */ +} +.ceb-28 { + background-color: #008700; /* 028 Green4 */ +} +.ceb-29 { + background-color: #00875f; /* 029 SpringGreen4 */ +} +.ceb-30 { + background-color: #008787; /* 030 Turquoise4 */ +} +.ceb-31 { + background-color: #0087af; /* 031 DeepSkyBlue3 */ +} +.ceb-32 { + background-color: #0087d7; /* 032 DeepSkyBlue3 */ +} +.ceb-33 { + background-color: #0087ff; /* 033 DodgerBlue1 */ +} +.ceb-34 { + background-color: #00af00; /* 034 Green3 */ +} +.ceb-35 { + background-color: #00af5f; /* 035 SpringGreen3 */ +} +.ceb-36 { + background-color: #00af87; /* 036 DarkCyan */ +} +.ceb-37 { + background-color: #00afaf; /* 037 LightSeaGreen */ +} +.ceb-38 { + background-color: #00afd7; /* 038 DeepSkyBlue2 */ +} +.ceb-39 { + background-color: #00afff; /* 039 DeepSkyBlue1 */ +} +.ceb-40 { + background-color: #00d700; /* 040 Green3 */ +} +.ceb-41 { + background-color: #00d75f; /* 041 SpringGreen3 */ +} +.ceb-42 { + background-color: #00d787; /* 042 SpringGreen2 */ +} +.ceb-43 { + background-color: #00d7af; /* 043 Cyan3 */ +} +.ceb-44 { + background-color: #00d7d7; /* 044 DarkTurquoise */ +} +.ceb-45 { + background-color: #00d7ff; /* 045 Turquoise2 */ +} +.ceb-46 { + background-color: #00ff00; /* 046 Green1 */ +} +.ceb-47 { + background-color: #00ff5f; /* 047 SpringGreen2 */ +} +.ceb-48 { + background-color: #00ff87; /* 048 SpringGreen1 */ +} +.ceb-49 { + background-color: #00ffaf; /* 049 MediumSpringGreen */ +} +.ceb-50 { + background-color: #00ffd7; /* 050 Cyan2 */ +} +.ceb-51 { + background-color: #00ffff; /* 051 Cyan1 */ +} +.ceb-52 { + background-color: #5f0000; /* 052 DarkRed */ +} +.ceb-53 { + background-color: #5f005f; /* 053 DeepPink4 */ +} +.ceb-54 { + background-color: #5f0087; /* 054 Purple4 */ +} +.ceb-55 { + background-color: #5f00af; /* 055 Purple4 */ +} +.ceb-56 { + background-color: #5f00d7; /* 056 Purple3 */ +} +.ceb-57 { + background-color: #5f00ff; /* 057 BlueViolet */ +} +.ceb-58 { + background-color: #5f5f00; /* 058 Orange4 */ +} +.ceb-59 { + background-color: #5f5f5f; /* 059 Grey37 */ +} +.ceb-60 { + background-color: #5f5f87; /* 060 MediumPurple4 */ +} +.ceb-61 { + background-color: #5f5faf; /* 061 SlateBlue3 */ +} +.ceb-62 { + background-color: #5f5fd7; /* 062 SlateBlue3 */ +} +.ceb-63 { + background-color: #5f5fff; /* 063 RoyalBlue1 */ +} +.ceb-64 { + background-color: #5f8700; /* 064 Chartreuse4 */ +} +.ceb-65 { + background-color: #5f875f; /* 065 DarkSeaGreen4 */ +} +.ceb-66 { + background-color: #5f8787; /* 066 PaleTurquoise4 */ +} +.ceb-67 { + background-color: #5f87af; /* 067 SteelBlue */ +} +.ceb-68 { + background-color: #5f87d7; /* 068 SteelBlue3 */ +} +.ceb-69 { + background-color: #5f87ff; /* 069 CornflowerBlue */ +} +.ceb-70 { + background-color: #5faf00; /* 070 Chartreuse3 */ +} +.ceb-71 { + background-color: #5faf5f; /* 071 DarkSeaGreen4 */ +} +.ceb-72 { + background-color: #5faf87; /* 072 CadetBlue */ +} +.ceb-73 { + background-color: #5fafaf; /* 073 CadetBlue */ +} +.ceb-74 { + background-color: #5fafd7; /* 074 SkyBlue3 */ +} +.ceb-75 { + background-color: #5fafff; /* 075 SteelBlue1 */ +} +.ceb-76 { + background-color: #5fd700; /* 076 Chartreuse3 */ +} +.ceb-77 { + background-color: #5fd75f; /* 077 PaleGreen3 */ +} +.ceb-78 { + background-color: #5fd787; /* 078 SeaGreen3 */ +} +.ceb-79 { + background-color: #5fd7af; /* 079 Aquamarine3 */ +} +.ceb-80 { + background-color: #5fd7d7; /* 080 MediumTurquoise */ +} +.ceb-81 { + background-color: #5fd7ff; /* 081 SteelBlue1 */ +} +.ceb-82 { + background-color: #5fff00; /* 082 Chartreuse2 */ +} +.ceb-83 { + background-color: #5fff5f; /* 083 SeaGreen2 */ +} +.ceb-84 { + background-color: #5fff87; /* 084 SeaGreen1 */ +} +.ceb-85 { + background-color: #5fffaf; /* 085 SeaGreen1 */ +} +.ceb-86 { + background-color: #5fffd7; /* 086 Aquamarine1 */ +} +.ceb-87 { + background-color: #5fffff; /* 087 DarkSlateGray2 */ +} +.ceb-88 { + background-color: #870000; /* 088 DarkRed */ +} +.ceb-89 { + background-color: #87005f; /* 089 DeepPink4 */ +} +.ceb-90 { + background-color: #870087; /* 090 DarkMagenta */ +} +.ceb-91 { + background-color: #8700af; /* 091 DarkMagenta */ +} +.ceb-92 { + background-color: #8700d7; /* 092 DarkViolet */ +} +.ceb-93 { + background-color: #8700ff; /* 093 Purple */ +} +.ceb-94 { + background-color: #875f00; /* 094 Orange4 */ +} +.ceb-95 { + background-color: #875f5f; /* 095 LightPink4 */ +} +.ceb-96 { + background-color: #875f87; /* 096 Plum4 */ +} +.ceb-97 { + background-color: #875faf; /* 097 MediumPurple3 */ +} +.ceb-98 { + background-color: #875fd7; /* 098 MediumPurple3 */ +} +.ceb-99 { + background-color: #875fff; /* 099 SlateBlue1 */ +} +.ceb-100 { + background-color: #878700; /* 100 Yellow4 */ +} +.ceb-101 { + background-color: #87875f; /* 101 Wheat4 */ +} +.ceb-102 { + background-color: #878787; /* 102 Grey53 */ +} +.ceb-103 { + background-color: #8787af; /* 103 LightSlateGrey */ +} +.ceb-104 { + background-color: #8787d7; /* 104 MediumPurple */ +} +.ceb-105 { + background-color: #8787ff; /* 105 LightSlateBlue */ +} +.ceb-106 { + background-color: #87af00; /* 106 Yellow4 */ +} +.ceb-107 { + background-color: #87af5f; /* 107 DarkOliveGreen3 */ +} +.ceb-108 { + background-color: #87af87; /* 108 DarkSeaGreen */ +} +.ceb-109 { + background-color: #87afaf; /* 109 LightSkyBlue3 */ +} +.ceb-110 { + background-color: #87afd7; /* 110 LightSkyBlue3 */ +} +.ceb-111 { + background-color: #87afff; /* 111 SkyBlue2 */ +} +.ceb-112 { + background-color: #87d700; /* 112 Chartreuse2 */ +} +.ceb-113 { + background-color: #87d75f; /* 113 DarkOliveGreen3 */ +} +.ceb-114 { + background-color: #87d787; /* 114 PaleGreen3 */ +} +.ceb-115 { + background-color: #87d7af; /* 115 DarkSeaGreen3 */ +} +.ceb-116 { + background-color: #87d7d7; /* 116 DarkSlateGray3 */ +} +.ceb-117 { + background-color: #87d7ff; /* 117 SkyBlue1 */ +} +.ceb-118 { + background-color: #87ff00; /* 118 Chartreuse1 */ +} +.ceb-119 { + background-color: #87ff5f; /* 119 LightGreen */ +} +.ceb-120 { + background-color: #87ff87; /* 120 LightGreen */ +} +.ceb-121 { + background-color: #87ffaf; /* 121 PaleGreen1 */ +} +.ceb-122 { + background-color: #87ffd7; /* 122 Aquamarine1 */ +} +.ceb-123 { + background-color: #87ffff; /* 123 DarkSlateGray1 */ +} +.ceb-124 { + background-color: #af0000; /* 124 Red3 */ +} +.ceb-125 { + background-color: #af005f; /* 125 DeepPink4 */ +} +.ceb-126 { + background-color: #af0087; /* 126 MediumVioletRed */ +} +.ceb-127 { + background-color: #af00af; /* 127 Magenta3 */ +} +.ceb-128 { + background-color: #af00d7; /* 128 DarkViolet */ +} +.ceb-129 { + background-color: #af00ff; /* 129 Purple */ +} +.ceb-130 { + background-color: #af5f00; /* 130 DarkOrange3 */ +} +.ceb-131 { + background-color: #af5f5f; /* 131 IndianRed */ +} +.ceb-132 { + background-color: #af5f87; /* 132 HotPink3 */ +} +.ceb-133 { + background-color: #af5faf; /* 133 MediumOrchid3 */ +} +.ceb-134 { + background-color: #af5fd7; /* 134 MediumOrchid */ +} +.ceb-135 { + background-color: #af5fff; /* 135 MediumPurple2 */ +} +.ceb-136 { + background-color: #af8700; /* 136 DarkGoldenrod */ +} +.ceb-137 { + background-color: #af875f; /* 137 LightSalmon3 */ +} +.ceb-138 { + background-color: #af8787; /* 138 RosyBrown */ +} +.ceb-139 { + background-color: #af87af; /* 139 Grey63 */ +} +.ceb-140 { + background-color: #af87d7; /* 140 MediumPurple2 */ +} +.ceb-141 { + background-color: #af87ff; /* 141 MediumPurple1 */ +} +.ceb-142 { + background-color: #afaf00; /* 142 Gold3 */ +} +.ceb-143 { + background-color: #afaf5f; /* 143 DarkKhaki */ +} +.ceb-144 { + background-color: #afaf87; /* 144 NavajoWhite3 */ +} +.ceb-145 { + background-color: #afafaf; /* 145 Grey69 */ +} +.ceb-146 { + background-color: #afafd7; /* 146 LightSteelBlue3 */ +} +.ceb-147 { + background-color: #afafff; /* 147 LightSteelBlue */ +} +.ceb-148 { + background-color: #afd700; /* 148 Yellow3 */ +} +.ceb-149 { + background-color: #afd75f; /* 149 DarkOliveGreen3 */ +} +.ceb-150 { + background-color: #afd787; /* 150 DarkSeaGreen3 */ +} +.ceb-151 { + background-color: #afd7af; /* 151 DarkSeaGreen2 */ +} +.ceb-152 { + background-color: #afd7d7; /* 152 LightCyan3 */ +} +.ceb-153 { + background-color: #afd7ff; /* 153 LightSkyBlue1 */ +} +.ceb-154 { + background-color: #afff00; /* 154 GreenYellow */ +} +.ceb-155 { + background-color: #afff5f; /* 155 DarkOliveGreen2 */ +} +.ceb-156 { + background-color: #afff87; /* 156 PaleGreen1 */ +} +.ceb-157 { + background-color: #afffaf; /* 157 DarkSeaGreen2 */ +} +.ceb-158 { + background-color: #afffd7; /* 158 DarkSeaGreen1 */ +} +.ceb-159 { + background-color: #afffff; /* 159 PaleTurquoise1 */ +} +.ceb-160 { + background-color: #d70000; /* 160 Red3 */ +} +.ceb-161 { + background-color: #d7005f; /* 161 DeepPink3 */ +} +.ceb-162 { + background-color: #d70087; /* 162 DeepPink3 */ +} +.ceb-163 { + background-color: #d700af; /* 163 Magenta3 */ +} +.ceb-164 { + background-color: #d700d7; /* 164 Magenta3 */ +} +.ceb-165 { + background-color: #d700ff; /* 165 Magenta2 */ +} +.ceb-166 { + background-color: #d75f00; /* 166 DarkOrange3 */ +} +.ceb-167 { + background-color: #d75f5f; /* 167 IndianRed */ +} +.ceb-168 { + background-color: #d75f87; /* 168 HotPink3 */ +} +.ceb-169 { + background-color: #d75faf; /* 169 HotPink2 */ +} +.ceb-170 { + background-color: #d75fd7; /* 170 Orchid */ +} +.ceb-171 { + background-color: #d75fff; /* 171 MediumOrchid1 */ +} +.ceb-172 { + background-color: #d78700; /* 172 Orange3 */ +} +.ceb-173 { + background-color: #d7875f; /* 173 LightSalmon3 */ +} +.ceb-174 { + background-color: #d78787; /* 174 LightPink3 */ +} +.ceb-175 { + background-color: #d787af; /* 175 Pink3 */ +} +.ceb-176 { + background-color: #d787d7; /* 176 Plum3 */ +} +.ceb-177 { + background-color: #d787ff; /* 177 Violet */ +} +.ceb-178 { + background-color: #d7af00; /* 178 Gold3 */ +} +.ceb-179 { + background-color: #d7af5f; /* 179 LightGoldenrod3 */ +} +.ceb-180 { + background-color: #d7af87; /* 180 Tan */ +} +.ceb-181 { + background-color: #d7afaf; /* 181 MistyRose3 */ +} +.ceb-182 { + background-color: #d7afd7; /* 182 Thistle3 */ +} +.ceb-183 { + background-color: #d7afff; /* 183 Plum2 */ +} +.ceb-184 { + background-color: #d7d700; /* 184 Yellow3 */ +} +.ceb-185 { + background-color: #d7d75f; /* 185 Khaki3 */ +} +.ceb-186 { + background-color: #d7d787; /* 186 LightGoldenrod2 */ +} +.ceb-187 { + background-color: #d7d7af; /* 187 LightYellow3 */ +} +.ceb-188 { + background-color: #d7d7d7; /* 188 Grey84 */ +} +.ceb-189 { + background-color: #d7d7ff; /* 189 LightSteelBlue1 */ +} +.ceb-190 { + background-color: #d7ff00; /* 190 Yellow2 */ +} +.ceb-191 { + background-color: #d7ff5f; /* 191 DarkOliveGreen1 */ +} +.ceb-192 { + background-color: #d7ff87; /* 192 DarkOliveGreen1 */ +} +.ceb-193 { + background-color: #d7ffaf; /* 193 DarkSeaGreen1 */ +} +.ceb-194 { + background-color: #d7ffd7; /* 194 Honeydew2 */ +} +.ceb-195 { + background-color: #d7ffff; /* 195 LightCyan1 */ +} +.ceb-196 { + background-color: #ff0000; /* 196 Red1 */ +} +.ceb-197 { + background-color: #ff005f; /* 197 DeepPink2 */ +} +.ceb-198 { + background-color: #ff0087; /* 198 DeepPink1 */ +} +.ceb-199 { + background-color: #ff00af; /* 199 DeepPink1 */ +} +.ceb-200 { + background-color: #ff00d7; /* 200 Magenta2 */ +} +.ceb-201 { + background-color: #ff00ff; /* 201 Magenta1 */ +} +.ceb-202 { + background-color: #ff5f00; /* 202 OrangeRed1 */ +} +.ceb-203 { + background-color: #ff5f5f; /* 203 IndianRed1 */ +} +.ceb-204 { + background-color: #ff5f87; /* 204 IndianRed1 */ +} +.ceb-205 { + background-color: #ff5faf; /* 205 HotPink */ +} +.ceb-206 { + background-color: #ff5fd7; /* 206 HotPink */ +} +.ceb-207 { + background-color: #ff5fff; /* 207 MediumOrchid1 */ +} +.ceb-208 { + background-color: #ff8700; /* 208 DarkOrange */ +} +.ceb-209 { + background-color: #ff875f; /* 209 Salmon1 */ +} +.ceb-210 { + background-color: #ff8787; /* 210 LightCoral */ +} +.ceb-211 { + background-color: #ff87af; /* 211 PaleVioletRed1 */ +} +.ceb-212 { + background-color: #ff87d7; /* 212 Orchid2 */ +} +.ceb-213 { + background-color: #ff87ff; /* 213 Orchid1 */ +} +.ceb-214 { + background-color: #ffaf00; /* 214 Orange1 */ +} +.ceb-215 { + background-color: #ffaf5f; /* 215 SandyBrown */ +} +.ceb-216 { + background-color: #ffaf87; /* 216 LightSalmon1 */ +} +.ceb-217 { + background-color: #ffafaf; /* 217 LightPink1 */ +} +.ceb-218 { + background-color: #ffafd7; /* 218 Pink1 */ +} +.ceb-219 { + background-color: #ffafff; /* 219 Plum1 */ +} +.ceb-220 { + background-color: #ffd700; /* 220 Gold1 */ +} +.ceb-221 { + background-color: #ffd75f; /* 221 LightGoldenrod2 */ +} +.ceb-222 { + background-color: #ffd787; /* 222 LightGoldenrod2 */ +} +.ceb-223 { + background-color: #ffd7af; /* 223 NavajoWhite1 */ +} +.ceb-224 { + background-color: #ffd7d7; /* 224 MistyRose1 */ +} +.ceb-225 { + background-color: #ffd7ff; /* 225 Thistle1 */ +} +.ceb-226 { + background-color: #ffff00; /* 226 Yellow1 */ +} +.ceb-227 { + background-color: #ffff5f; /* 227 LightGoldenrod1 */ +} +.ceb-228 { + background-color: #ffff87; /* 228 Khaki1 */ +} +.ceb-229 { + background-color: #ffffaf; /* 229 Wheat1 */ +} +.ceb-230 { + background-color: #ffffd7; /* 230 Cornsilk1 */ +} +.ceb-231 { + background-color: #ffffff; /* 231 Grey100 */ +} +.ceb-232 { + background-color: #080808; /* 232 Grey3 */ +} +.ceb-233 { + background-color: #121212; /* 233 Grey7 */ +} +.ceb-234 { + background-color: #1c1c1c; /* 234 Grey11 */ +} +.ceb-235 { + background-color: #262626; /* 235 Grey15 */ +} +.ceb-236 { + background-color: #303030; /* 236 Grey19 */ +} +.ceb-237 { + background-color: #3a3a3a; /* 237 Grey23 */ +} +.ceb-238 { + background-color: #444444; /* 238 Grey27 */ +} +.ceb-239 { + background-color: #4e4e4e; /* 239 Grey30 */ +} +.ceb-240 { + background-color: #585858; /* 240 Grey35 */ +} +.ceb-241 { + background-color: #626262; /* 241 Grey39 */ +} +.ceb-242 { + background-color: #6c6c6c; /* 242 Grey42 */ +} +.ceb-243 { + background-color: #767676; /* 243 Grey46 */ +} +.ceb-244 { + background-color: #808080; /* 244 Grey50 */ +} +.ceb-245 { + background-color: #8a8a8a; /* 245 Grey54 */ +} +.ceb-246 { + background-color: #949494; /* 246 Grey58 */ +} +.ceb-247 { + background-color: #9e9e9e; /* 247 Grey62 */ +} +.ceb-248 { + background-color: #a8a8a8; /* 248 Grey66 */ +} +.ceb-249 { + background-color: #b2b2b2; /* 249 Grey70 */ +} +.ceb-250 { + background-color: #bcbcbc; /* 250 Grey74 */ +} +.ceb-251 { + background-color: #c6c6c6; /* 251 Grey78 */ +} +.ceb-252 { + background-color: #d0d0d0; /* 252 Grey82 */ +} +.ceb-253 { + background-color: #dadada; /* 253 Grey85 */ +} +.ceb-254 { + background-color: #e4e4e4; /* 254 Grey89 */ +} +.ceb-255 { + background-color: #eeeeee; /* 255 Grey93 */ +} + +/* attributes overrides */ +.a-b { + font-weight: bold; +} +.a-no-b { + font-weight: normal; +} +.a-i { + font-style: italic; +} +.a-no-i { + font-style: normal; +} +.a-u { + text-decoration: underline; +} +.a-no-u { + text-decoration: none; +} +.a-reset { + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +/* highlight messages */ +.highlight { + color: orange; + font-weight: bold; +} + +/* */ +/* Mobile layout */ +/* */ +@media (max-width: 968px) { + /* a different colour is too irregular on mobile */ + .repeated-time .cof-chat_time, + .repeated-time .cof-chat_time_delimiters { + color: #666; + } + + #nicklist { + -webkit-box-shadow: 0px 0px 120px #999; + box-shadow: 0px 0px 120px #999; + background: #e7e7e7; + + } + + .footer { + background: rgb(230,230,230); + } + +} diff --git a/sources/directives/input.html b/sources/directives/input.html new file mode 100644 index 0000000..030708f --- /dev/null +++ b/sources/directives/input.html @@ -0,0 +1,14 @@ +

    +
    + + + + + +
    +
    + diff --git a/sources/directives/plugin.html b/sources/directives/plugin.html new file mode 100644 index 0000000..0ae78eb --- /dev/null +++ b/sources/directives/plugin.html @@ -0,0 +1,18 @@ +
    + + +
    +
    + +
    + +
    diff --git a/sources/index.html b/sources/index.html new file mode 100644 index 0000000..e122eda --- /dev/null +++ b/sources/index.html @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Upload error: Image upload failed.

    +
    +
    +

    + logo + Glowing Bear + WeeChat web frontend +

    +
    + Connection error The client was unable to connect to the WeeChat relay +
    +
    + Secure connection error A secure connection with the WeeChat relay could not be initiated. This is most likely because your browser does not trust your relay's certificate. Please read the encryption instructions below! +
    +
    + Secure connection error Unable to connect to unencrypted relay when your are connecting to Glowing Bear over HTTPS. Please use an encrypted relay or load the page without using HTTPS. +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +

    Configuring the relay

    +
    To start using glowing bear, please enable the relay plugin in your WeeChat client: +
    +/set relay.network.password yourpassword
    +/relay add weechat {{ settings.port || 9001 }}
    +
    + WeeChat version 0.4.2 or higher is required.
    + The communication goes directly between your browser and your WeeChat relay in plain text. Check the instructions below for help on setting up encrypted communication. + Connection settings, including your password, are saved locally in your own browser between sessions. +
    +

    Shortcuts

    + Glowing Bear has a few shortcuts: +
      +
    • ALT-n: Toggle nicklist
    • +
    • ALT-l: Focus on input bar
    • +
    • ALT-[0-9]: Switch to buffer number N
    • +
    • ALT-a: Focus on next buffer with activity
    • +
    • ALT-<: Switch to previous active buffer
    • +
    • ALT-g: Focus on buffer list filter
    • +
    • Esc-Esc: Disconnect (double-tap)
    • +
    • Arrow keys: Navigate history
    • +
    • Tab: Complete nick
    • +
    • The following readline/emacs style keybindings can be enabled with a setting: Ctrl-a, Ctrl-e, Ctrl-u, Ctrl-k, Ctrl-w
    • +
    +
    +
    +
    +
    +
    + +
    +
    +

    If you check the encryption box, the communication between browser and WeeChat will be encrypted with TLS.

    +

    Note: If you are using a self-signed certificate, you have to visit https://{{ settings.host || 'weechathost' }}:{{ settings.port || 'relayport' }}/weechat in your browser first to add a security exception. You can close that tab once you confirmed the certificate, no content will appear. The necessity of this process is a bug in Firefox and other browsers.

    +

    Setup: If you want to use an encrypted session you first have to set up the relay to use TLS. You basically have two options: a self-signed certificate is easier to set up, but requires manual security exceptions. Using a certificate that is trusted by your browser requires more setup, but offers greater convenience later on and does not require security exceptions. You can find a guide to set up WeeChat with a free trusted certificate from StartSSL here. Should you wish to use a self-signed certificate instead, execute the following commands in a shell on the same host and as the user running WeeChat:

    +
    +$ mkdir -p ~/.weechat/ssl
    +$ cd ~/.weechat/ssl
    +$ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out relay.pem -sha256 -subj "/CN={{settings.host || 'your weechat host'}}/"
    +
    +

    If WeeChat is already running, you can reload the certificate and private key and set up an encrypted relay on port {{ settings.port || 9001 }} with these WeeChat commands:

    +
    +/set relay.network.password yourpassword
    +/relay sslcertkey
    +/relay add ssl.weechat {{ settings.port || 9001 }}
    +
    +
    +
    +
    +
    + +
    +
    +

    You don't need to install anything to use this app, it should work with any modern browser. Start using it right now! However, there are a few ways to improve integration with your operating system.

    +

    Mobile Applications

    +

    If you're running Android 4.4 or later, you can install our app from the Google Play Store! We also provide an optimized application for Firefox OS devices. If you're using the Firefox browser, keep on reading below -- the Firefox OS app won't work for you

    +

    Android app on Google Play Firefox OS app in the Firefox Marketplace

    +

    Firefox Browser

    +

    If you have a recent version of Firefox you can install Glowing Bear as a Firefox app. Click the button to install.

    +

    +

    Note for self-signed certificates: Firefox does not share a certificate storage with Firefox apps, so accepting self-signed certificates is a bit tricky.

    +

    Chrome

    +

    To install Glowing Bear as an app in Chrome for Android, select Menu - Add to home screen. In the desktop version of Chrome, click Menu - More tools - Create application shortcuts.

    +
    +
    +
    +
    + +
    +
    +

    Glowing bear is built by a small group of developers in their free time. As we're always trying to improve it, we would love getting your feedback and help. If that sounds like something you might enjoy, check out our project page on GitHub!

    +

    If you're interested in contributing or simply want to say hello, head over to #glowing-bear on freenode! We won't bite, promise (-ish).

    +
    +
    +
    +
    +
    +
    +
    +
    + + brand + + {{unread}} + {{notifications}} + + +
    + +
    + + {{ activeBuffer().trimmedName || activeBuffer().fullName }} +
    + + +
    + +
    +
    +
      +
    • + +
    • +
    +
    + + + + + + + + + + + + + + + +
    + Fetch more lines + Fetching more lines +
    + + :: + + <>
    +
    +
    +
    +
    + +
    +
    +
    +

    Connection to WeeChat lost

    + + Reconnecting... Click to try to reconnect now +
    +
    +
    + +
    +
    +
    + +
    + + diff --git a/sources/js/connection.js b/sources/js/connection.js new file mode 100644 index 0000000..319219f --- /dev/null +++ b/sources/js/connection.js @@ -0,0 +1,387 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat'); + +weechat.factory('connection', + ['$rootScope', '$log', 'handlers', 'models', 'ngWebsockets', function($rootScope, + $log, + handlers, + models, + ngWebsockets) { + + var protocol = new weeChat.Protocol(); + + var connectionData = []; + var reconnectTimer; + + // Takes care of the connection and websocket hooks + var connect = function (host, port, passwd, ssl, noCompression, successCallback, failCallback) { + $rootScope.passwordError = false; + connectionData = [host, port, passwd, ssl, noCompression]; + var proto = ssl ? 'wss' : 'ws'; + // If host is an IPv6 literal wrap it in brackets + if (host.indexOf(":") !== -1 && host[0] !== "[" && host[host.length-1] !== "]") { + host = "[" + host + "]"; + } + var url = proto + "://" + host + ":" + port + "/weechat"; + $log.debug('Connecting to URL: ', url); + + var onopen = function () { + + + // Helper methods for initialization commands + var _initializeConnection = function(passwd) { + // This is not the proper way to do this. + // WeeChat does not send a confirmation for the init. + // Until it does, We need to "assume" that formatInit + // will be received before formatInfo + ngWebsockets.send( + weeChat.Protocol.formatInit({ + password: passwd, + compression: noCompression ? 'off' : 'zlib' + }) + ); + + return ngWebsockets.send( + weeChat.Protocol.formatInfo({ + name: 'version' + }) + ); + }; + + var _requestHotlist = function() { + return ngWebsockets.send( + weeChat.Protocol.formatHdata({ + path: "hotlist:gui_hotlist(*)", + keys: [] + }) + ); + }; + + var _requestBufferInfos = function() { + return ngWebsockets.send( + weeChat.Protocol.formatHdata({ + path: 'buffer:gui_buffers(*)', + keys: ['local_variables,notify,number,full_name,short_name,title,hidden,type'] + }) + ); + }; + + var _requestSync = function() { + return ngWebsockets.send( + weeChat.Protocol.formatSync({}) + ); + }; + + + // First command asks for the password and issues + // a version command. If it fails, it means the we + // did not provide the proper password. + _initializeConnection(passwd).then( + function(version) { + handlers.handleVersionInfo(version); + // Connection is successful + // Send all the other commands required for initialization + _requestBufferInfos().then(function(bufinfo) { + handlers.handleBufferInfo(bufinfo); + }); + + _requestHotlist().then(function(hotlist) { + handlers.handleHotlistInfo(hotlist); + + if (successCallback) { + successCallback(); + } + }); + + _requestSync(); + $log.info("Connected to relay"); + $rootScope.connected = true; + }, + function() { + handleWrongPassword(); + } + ); + + }; + + var onmessage = function() { + // If we recieve a message from WeeChat it means that + // password was OK. Store that result and check for it + // in the failure handler. + $rootScope.waseverconnected = true; + }; + + + var onclose = function (evt) { + /* + * Handles websocket disconnection + */ + $log.info("Disconnected from relay"); + $rootScope.$emit('relayDisconnect'); + if ($rootScope.userdisconnect || !$rootScope.waseverconnected) { + handleClose(evt); + $rootScope.userdisconnect = false; + } else { + reconnect(evt); + } + handleWrongPassword(); + }; + + var handleClose = function (evt) { + if (ssl && evt && evt.code === 1006) { + // A password error doesn't trigger onerror, but certificate issues do. Check time of last error. + if (typeof $rootScope.lastError !== "undefined" && (Date.now() - $rootScope.lastError) < 1000) { + // abnormal disconnect by client, most likely ssl error + $rootScope.sslError = true; + $rootScope.$apply(); + } + } + }; + + var handleWrongPassword = function() { + // Connection got closed, lets check if we ever was connected successfully + if (!$rootScope.waseverconnected && !$rootScope.errorMessage) { + $rootScope.passwordError = true; + $rootScope.$apply(); + } + }; + + var onerror = function (evt) { + /* + * Handles cases when connection issues come from + * the relay. + */ + $log.error("Relay error", evt); + $rootScope.lastError = Date.now(); + + if (evt.type === "error" && this.readyState !== 1) { + ngWebsockets.failCallbacks('error'); + $rootScope.errorMessage = true; + } + }; + + try { + ngWebsockets.connect(url, + protocol, + { + 'binaryType': "arraybuffer", + 'onopen': onopen, + 'onclose': onclose, + 'onmessage': onmessage, + 'onerror': onerror + }); + } catch(e) { + $log.debug("Websocket caught DOMException:", e); + $rootScope.lastError = Date.now(); + $rootScope.errorMessage = true; + $rootScope.securityError = true; + $rootScope.$emit('relayDisconnect'); + + if (failCallback) { + failCallback(); + } + } + + }; + + var attemptReconnect = function (bufferId, timeout) { + $log.info('Attempting to reconnect...'); + var d = connectionData; + connect(d[0], d[1], d[2], d[3], d[4], function() { + $rootScope.reconnecting = false; + // on success, update active buffer + models.setActiveBuffer(bufferId); + $log.info('Sucessfully reconnected to relay'); + }, function() { + // on failure, schedule another attempt + if (timeout >= 600000) { + // If timeout is ten minutes or more, give up + $log.info('Failed to reconnect, giving up'); + handleClose(); + } else { + $log.info('Failed to reconnect, scheduling next attempt in', timeout/1000, 'seconds'); + // Clear previous timer, if exists + if (reconnectTimer !== undefined) { + clearTimeout(reconnectTimer); + } + reconnectTimer = setTimeout(function() { + // exponential timeout increase + attemptReconnect(bufferId, timeout * 1.5); + }, timeout); + } + }); + }; + + + var reconnect = function (evt) { + if (connectionData.length < 5) { + // something is wrong + $log.error('Cannot reconnect, connection information is missing'); + return; + } + + // reinitialise everything, clear all buffers + // TODO: this can be further extended in the future by looking + // at the last line in ever buffer and request more buffers from + // WeeChat based on that + models.reinitialize(); + $rootScope.reconnecting = true; + // Have to do this to get the reconnect banner to show + $rootScope.$apply(); + + var bufferId = models.getActiveBuffer().id, + timeout = 3000; // start with a three-second timeout + + reconnectTimer = setTimeout(function() { + attemptReconnect(bufferId, timeout); + }, timeout); + }; + + var disconnect = function() { + $log.info('Disconnecting from relay'); + $rootScope.userdisconnect = true; + ngWebsockets.send(weeChat.Protocol.formatQuit()); + // In case the backend doesn't repond we will close from our end + var closeTimer = setTimeout(function() { + ngWebsockets.disconnect(); + // We pretend we are not connected anymore + // The connection can time out on its own + ngWebsockets.failCallbacks('disconnection'); + $rootScope.connected = false; + $rootScope.$emit('relayDisconnect'); + $rootScope.$apply(); + }); + }; + + /* + * Format and send a weechat message + * + * @returns the angular promise + */ + var sendMessage = function(message) { + ngWebsockets.send(weeChat.Protocol.formatInput({ + buffer: models.getActiveBufferReference(), + data: message + })); + }; + + var sendCoreCommand = function(command) { + ngWebsockets.send(weeChat.Protocol.formatInput({ + buffer: 'core.weechat', + data: command + })); + }; + + var sendHotlistClear = function() { + if (models.version[0] >= 1) { + // WeeChat >= 1 supports clearing hotlist with this command + sendMessage('/buffer set hotlist -1'); + // Also move read marker + sendMessage('/input set_unread_current_buffer'); + } else { + // If user wants to sync hotlist with weechat + // we will send a /buffer bufferName command every time + // the user switches a buffer. This will ensure that notifications + // are cleared in the buffer the user switches to + sendCoreCommand('/buffer ' + models.getActiveBuffer().fullName); + } + }; + + var requestNicklist = function(bufferId, callback) { + // Prevent requesting nicklist for all buffers if bufferId is invalid + if (!bufferId) { + return; + } + ngWebsockets.send( + weeChat.Protocol.formatNicklist({ + buffer: "0x"+bufferId + }) + ).then(function(nicklist) { + handlers.handleNicklist(nicklist); + if (callback !== undefined) { + callback(); + } + }); + }; + + var fetchConfValue = function(name) { + ngWebsockets.send( + weeChat.Protocol.formatInfolist({ + name: "option", + pointer: 0, + args: name + }) + ).then(function(i) { + handlers.handleConfValue(i); + }); + }; + + var fetchMoreLines = function(numLines) { + $log.debug('Fetching ', numLines, ' lines'); + var buffer = models.getActiveBuffer(); + if (numLines === undefined) { + // Math.max(undefined, *) = NaN -> need a number here + numLines = 0; + } + // Calculate number of lines to fetch, at least as many as the parameter + numLines = Math.max(numLines, buffer.requestedLines * 2); + + // Indicator that we are loading lines, hides "load more lines" link + $rootScope.loadingLines = true; + // Send hdata request to fetch lines for this particular buffer + return ngWebsockets.send( + weeChat.Protocol.formatHdata({ + // "0x" is important, otherwise it won't work + path: "buffer:0x" + buffer.id + "/own_lines/last_line(-" + numLines + ")/data", + keys: [] + }) + ).then(function(lineinfo) { +//XXX move to handlers? + // delete old lines and add new ones + var oldLength = buffer.lines.length; + // whether we already had all unread lines + var hadAllUnreadLines = buffer.lastSeen >= 0; + + // clear the old lines + buffer.lines.length = 0; + // We need to set the number of requested lines to 0 here, because parsing a line + // increments it. This is needed to also count newly arriving lines while we're + // already connected. + buffer.requestedLines = 0; + // Count number of lines recieved + var linesReceivedCount = lineinfo.objects[0].content.length; + + // Parse the lines + handlers.handleLineInfo(lineinfo, true); + + // Correct the read marker for the lines that were counted twice + buffer.lastSeen -= oldLength; + + // We requested more lines than we got, no more lines. + if (linesReceivedCount < numLines) { + buffer.allLinesFetched = true; + } + $rootScope.loadingLines = false; + + // Only scroll to read marker if we didn't have all unread lines previously, but have them now + var scrollToReadmarker = !hadAllUnreadLines && buffer.lastSeen >= 0; + // Scroll to correct position + $rootScope.scrollWithBuffer(scrollToReadmarker, true); + }); + }; + + + return { + connect: connect, + disconnect: disconnect, + sendMessage: sendMessage, + sendCoreCommand: sendCoreCommand, + sendHotlistClear: sendHotlistClear, + fetchMoreLines: fetchMoreLines, + requestNicklist: requestNicklist, + attemptReconnect: attemptReconnect + }; +}]); +})(); diff --git a/sources/js/file-change.js b/sources/js/file-change.js new file mode 100644 index 0000000..1b22359 --- /dev/null +++ b/sources/js/file-change.js @@ -0,0 +1,23 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat'); + +weechat.directive('fileChange', ['$parse', function($parse) { + + return { + restrict: 'A', + link: function ($scope, element, attrs) { + var attrHandler = $parse(attrs.fileChange); + var handler = function (e) { + $scope.$apply(function () { + attrHandler($scope, { $event: e, files: e.target.files }); + }); + }; + element[0].addEventListener('change', handler, false); + } + }; + + }]); + +})(); diff --git a/sources/js/filters.js b/sources/js/filters.js new file mode 100644 index 0000000..e51f0a2 --- /dev/null +++ b/sources/js/filters.js @@ -0,0 +1,205 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat'); + +weechat.filter('toArray', function () { + return function (obj, storeIdx) { + if (!(obj instanceof Object)) { + return obj; + } + + if (storeIdx) { + return Object.keys(obj).map(function (key, idx) { + return Object.defineProperties(obj[key], { + '$key' : { value: key }, + '$idx' : { value: idx, configurable: true } + }); + }); + } + + return Object.keys(obj).map(function (key) { + return Object.defineProperty(obj[key], '$key', { value: key }); + }); + }; +}); + +weechat.filter('irclinky', function() { + return function(text) { + if (!text) { + return text; + } + + // This regex in no way matches all IRC channel names (they could also begin with &, + or an + // exclamation mark followed by 5 alphanumeric characters, and are bounded in length by 50). + // However, it matches all *common* IRC channels while trying to minimise false positives. + // "#1" is much more likely to be "number 1" than "IRC channel #1". + // Thus, we only match channels beginning with a # and having at least one letter in them. + var channelRegex = /(^|[\s,.:;?!"'()+@-\~%])(#+[^\x00\x07\r\n\s,:]*[a-z][^\x00\x07\r\n\s,:]*)/gmi; + // Call the method we bound to window.openBuffer when we instantiated + // the Weechat controller. + var substitute = '$1$2'; + return text.replace(channelRegex, substitute); + }; +}); + +weechat.filter('inlinecolour', function() { + return function(text) { + if (!text) { + return text; + } + + // only match 6-digit colour codes, 3-digit ones have too many false positives (issue numbers, etc) + var hexColourRegex = /(^|[^&])(\#[0-9a-f]{6};?)(?!\w)/gmi; + var rgbColourRegex = /(.?)(rgba?\((?:\s*\d+\s*,){2}\s*\d+\s*(?:,\s*[\d.]+\s*)?\);?)/gmi; + var substitute = '$1$2
    '; + text = text.replace(hexColourRegex, substitute); + text = text.replace(rgbColourRegex, substitute); + return text; + }; +}); + +// apply a filter to an HTML string's text nodes, and do so with not exceedingly terrible performance +weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) { + // To prevent nested anchors, we need to know if a filter is going to create them. + // Here's a list of names. See #681 for more information. + var filtersThatCreateAnchors = ['irclinky']; + + return function(text, filter) { + if (!text || !filter) { + return text; + } + var createsAnchor = filtersThatCreateAnchors.indexOf(filter) > -1; + + var escape_html = function(text) { + // First, escape entities to prevent escaping issues because it's a bad idea + // to parse/modify HTML with regexes, which we do a couple of lines down... + var entities = {"<": "<", ">": ">", '"': '"', "'": ''', "&": "&", "/": '/'}; + return text.replace(/[<>"'&\/]/g, function (char) { + return entities[char]; + }); + }; + + // hacky way to pass extra arguments without using .apply, which + // would require assembling an argument array. PERFORMANCE!!! + var extraArgument = (arguments.length > 2) ? arguments[2] : null; + var thirdArgument = (arguments.length > 3) ? arguments[3] : null; + + var filterFunction = $filter(filter); + var el = document.createElement('div'); + el.innerHTML = text; + + // Recursive DOM-walking function applying the filter to the text nodes + var process = function(node) { + if (node.nodeType === 3) { // text node + // apply the filter to *escaped* HTML, and only commit changes if + // it changed the escaped value. This is because setting the result + // as innerHTML causes it to be unescaped. + var input = escape_html(node.nodeValue); + var value = filterFunction(input, extraArgument, thirdArgument); + + if (value !== input) { + // we changed something. create a new node to replace the current one + // we could also only add its children but that would probably incur + // more overhead than it would gain us + var newNode = document.createElement('span'); + newNode.innerHTML = value; + + var parent = node.parentNode; + var sibling = node.nextSibling; + parent.removeChild(node); + if (sibling) { + parent.insertBefore(newNode, sibling); + } else { + parent.appendChild(newNode); + } + return newNode; + } + } + // recurse + if (node === undefined || node === null) return; + node = node.firstChild; + while (node) { + var nextNode = null; + // do not recurse inside links if the filter would create a nested link + if (!(createsAnchor && node.tagName === 'A')) { + nextNode = process(node); + } + node = (nextNode ? nextNode : node).nextSibling; + } + }; + + process(el); + + return $sce.trustAsHtml(el.innerHTML); + }; +}]); + +weechat.filter('getBufferQuickKeys', function () { + return function (obj, $scope) { + if (!$scope) { return obj; } + if (($scope.search !== undefined && $scope.search.length) || $scope.onlyUnread) { + obj.forEach(function(buf, idx) { + buf.$quickKey = idx < 10 ? (idx + 1) % 10 : ''; + }); + } else { + _.map(obj, function(buffer, idx) { + return [buffer.number, buffer.$idx, idx]; + }).sort(function(left, right) { + // By default, Array.prototype.sort() sorts alphabetically. + // Pass an ordering function to sort by first element. + return left[0] - right[0] || left[1] - right[1]; + }).forEach(function(info, keyIdx) { + obj[ info[2] ].$quickKey = keyIdx < 10 ? (keyIdx + 1) % 10 : ''; + }); + } + return obj; + }; +}); + +// Emojifis the string using https://github.com/Ranks/emojione +weechat.filter('emojify', function() { + return function(text, enable_JS_Emoji) { + if (enable_JS_Emoji === true && window.emojione !== undefined) { + // Emoji live in the D800-DFFF surrogate plane; only bother passing + // this range to CPU-expensive unicodeToImage(); + var emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; + if (emojiRegex.test(text)) { + return emojione.unicodeToImage(text); + } else { + return(text); + } + } else { + return(text); + } + }; +}); + +weechat.filter('mathjax', function() { + return function(text, selector, enabled) { + if (!enabled || typeof(MathJax) === "undefined") { + return text; + } + if (text.indexOf("$$") != -1 || text.indexOf("\\[") != -1 || text.indexOf("\\(") != -1) { + // contains math + var math = document.querySelector(selector); + MathJax.Hub.Queue(["Typeset",MathJax.Hub,math]); + } + + return text; + }; +}); + +weechat.filter('prefixlimit', function() { + return function(input, chars) { + if (isNaN(chars)) return input; + if (chars <= 0) return ''; + if (input && input.length > chars) { + input = input.substring(0, chars); + return input + '+'; + } + return input; + }; +}); + +})(); diff --git a/sources/js/glowingbear.js b/sources/js/glowingbear.js new file mode 100644 index 0000000..271916a --- /dev/null +++ b/sources/js/glowingbear.js @@ -0,0 +1,848 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'IrcUtils', 'ngSanitize', 'ngWebsockets', 'ngTouch'], ['$compileProvider', function($compileProvider) { + // hacky way to be able to find out if we're in debug mode + weechat.compileProvider = $compileProvider; +}]); +weechat.config(['$compileProvider', function ($compileProvider) { + // hack to determine whether we're executing the tests + if (typeof(it) === "undefined" && typeof(describe) === "undefined") { + $compileProvider.debugInfoEnabled(false); + } +}]); + +weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', 'settings', + function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils, settings) { + + window.openBuffer = function(channel) { + $scope.openBuffer(channel); + $scope.$apply(); + }; + + $scope.command = ''; + $scope.themes = ['dark', 'light', 'black']; + + // Initialise all our settings, this needs to include all settings + // or else they won't be saved to the localStorage. + settings.setDefaults({ + 'theme': 'dark', + 'host': 'localhost', + 'port': 9001, + 'ssl': (window.location.protocol === "https:"), + 'savepassword': false, + 'autoconnect': false, + 'nonicklist': utils.isMobileUi(), + 'noembed': true, + 'onlyUnread': false, + 'hotlistsync': true, + 'orderbyserver': true, + 'useFavico': true, + 'showtimestamp': true, + 'showtimestampSeconds': false, + 'soundnotification': true, + 'fontsize': '14px', + 'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'), + 'readlineBindings': false, + 'enableJSEmoji': (utils.isMobileUi() ? false : true), + 'enableMathjax': false, + 'customCSS': '', + }); + $scope.settings = settings; + + $rootScope.countWatchers = function () { + $log.debug($rootScope.$$watchersCount); + }; + + $scope.isinstalled = (function() { + // Check for firefox & app installed + if (navigator.mozApps !== undefined) { + navigator.mozApps.getSelf().onsuccess = function _onAppReady(evt) { + var app = evt.target.result; + if (app) { + return true; + } else { + return false; + } + }; + } else { + return false; + } + }()); + + + // Detect page visibility attributes + (function() { + // Sadly, the page visibility API still has a lot of vendor prefixes + if (typeof document.hidden !== "undefined") { // Chrome >= 33, Firefox >= 18, Opera >= 12.10, Safari >= 7 + $scope.documentHidden = "hidden"; + $scope.documentVisibilityChange = "visibilitychange"; + } else if (typeof document.webkitHidden !== "undefined") { // 13 <= Chrome < 33 + $scope.documentHidden = "webkitHidden"; + $scope.documentVisibilityChange = "webkitvisibilitychange"; + } else if (typeof document.mozHidden !== "undefined") { // 10 <= Firefox < 18 + $scope.documentHidden = "mozHidden"; + $scope.documentVisibilityChange = "mozvisibilitychange"; + } else if (typeof document.msHidden !== "undefined") { // IE >= 10 + $scope.documentHidden = "msHidden"; + $scope.documentVisibilityChange = "msvisibilitychange"; + } + })(); + + // Enable debug mode if "?debug=1" or "?debug=true" is set + (function() { + window.location.search.substring(1).split('&').forEach(function(f) { + var segs = f.split('='); + if (segs[0] === "debug" && ["true", "1"].indexOf(segs[1]) != -1) { + $rootScope.debugMode = true; + } + }); + // If we haven't reloaded yet, do an angular reload with debug infos + // store whether this has happened yet in a GET parameter + if ($rootScope.debugMode && !weechat.compileProvider.debugInfoEnabled()) { + angular.reloadWithDebugInfo(); + } + })(); + + + $rootScope.isWindowFocused = function() { + if (typeof $scope.documentHidden === "undefined") { + // Page Visibility API not supported, assume yes + return true; + } else { + var isHidden = document[$scope.documentHidden]; + return !isHidden; + } + }; + + if (typeof $scope.documentVisibilityChange !== "undefined") { + document.addEventListener($scope.documentVisibilityChange, function() { + if (!document[$scope.documentHidden]) { + // We just switched back to the glowing-bear window and unread messages may have + // accumulated in the active buffer while the window was in the background + var buffer = models.getActiveBuffer(); + // This can also be triggered before connecting to the relay, check for null (not undefined!) + if (buffer !== null) { + buffer.unread = 0; + buffer.notification = 0; + + // Trigger title and favico update + $rootScope.$emit('notificationChanged'); + } + + // the unread badge in the bufferlist doesn't update if we don't do this + $rootScope.$apply(); + } + }, false); + } + + + $rootScope.$on('activeBufferChanged', function(event, unreadSum) { + var ab = models.getActiveBuffer(); + + // Discard surplus lines. This is done *before* lines are fetched because that saves us the effort of special handling for the + // case where a buffer is opened for the first time ;) + var minRetainUnread = ab.lines.length - unreadSum + 5; // do not discard unread lines and keep 5 additional lines for context + var surplusLines = ab.lines.length - (2 * $scope.lines_per_screen + 10); // retain up to 2*(screenful + 10) + 10 lines because magic numbers + var linesToRemove = Math.min(minRetainUnread, surplusLines); + + if (linesToRemove > 0) { + ab.lines.splice(0, linesToRemove); // remove the lines from the buffer + ab.requestedLines -= linesToRemove; // to ensure that the correct amount of lines is fetched should more be requested + ab.lastSeen -= linesToRemove; // adjust readmarker + ab.allLinesFetched = false; // we just removed lines, so we don't have all of them. re-enable "fetch more lines" + } + + $scope.bufferlines = ab.lines; + $scope.nicklist = ab.nicklist; + + // Send a request for the nicklist if it hasn't been loaded yet + if (!ab.nicklistRequested()) { + connection.requestNicklist(ab.id, function() { + $scope.showNicklist = $scope.updateShowNicklist(); + // Scroll after nicklist has been loaded, as it may break long lines + $rootScope.scrollWithBuffer(true); + }); + } else { + // Check if we should show nicklist or not + $scope.showNicklist = $scope.updateShowNicklist(); + } + + if (ab.requestedLines < $scope.lines_per_screen) { + // buffer has not been loaded, but some lines may already be present if they arrived after we connected + // try to determine how many lines to fetch + var numLines = $scope.lines_per_screen + 10; // that's (a screenful plus 10 lines) plus 10 lines, just to be safe + if (unreadSum > numLines) { + // request up to 4*(screenful + 10 lines) + numLines = Math.min(4*numLines, unreadSum); + } + $scope.fetchMoreLines(numLines).then( + // Update initial scroll position + // Most relevant when first connecting to properly initalise + function() { + $timeout(function() { + var bl = document.getElementById("bufferlines"); + var lastScrollHeight = bl.scrollHeight; + var scrollHeightObserver = function() { + if (bl) { + var newScrollHeight = bl.scrollHeight; + if (newScrollHeight !== lastScrollHeight) { + $rootScope.updateBufferBottom($rootScope.bufferBottom); + lastScrollHeight = newScrollHeight; + } + setTimeout(scrollHeightObserver, 500); + } + }; + $rootScope.updateBufferBottom(true); + $rootScope.scrollWithBuffer(true); + bl.onscroll = _.debounce(function() { + $rootScope.updateBufferBottom(); + }, 80); + setTimeout(scrollHeightObserver, 500); + }); + } + ); + } + notifications.updateTitle(ab); + setTimeout(function(){ + $scope.notifications = notifications.unreadCount('notification'); + $scope.unread = notifications.unreadCount('unread'); + }); + + $timeout(function() { + $rootScope.scrollWithBuffer(true); + }); + + // Clear search term on buffer change + $scope.search = ''; + + if (!utils.isMobileUi()) { + // This needs to happen asynchronously to prevent the enter key handler + // of the input bar to be triggered on buffer switch via the search. + // Otherwise its current contents would be sent to the new buffer + setTimeout(function() { + document.getElementById('sendMessage').focus(); + }, 0); + } + + // Do this part last since it's not important for the UI + if (settings.hotlistsync && ab.fullName) { + connection.sendHotlistClear(); + } + }); + + $rootScope.favico = new Favico({animation: 'none'}); + $scope.notifications = notifications.unreadCount('notification'); + $scope.unread = notifications.unreadCount('unread'); + + $rootScope.$on('notificationChanged', function() { + notifications.updateTitle(); + $scope.notifications = notifications.unreadCount('notification'); + $scope.unread = notifications.unreadCount('unread'); + + if (settings.useFavico && $rootScope.favico) { + notifications.updateFavico(); + } + }); + + $rootScope.$on('relayDisconnect', function() { + // Reset title + $rootScope.pageTitle = ''; + $rootScope.notificationStatus = ''; + notifications.cancelAll(); + + models.reinitialize(); + $rootScope.$emit('notificationChanged'); + $scope.connectbutton = 'Connect'; + $scope.connectbuttonicon = 'glyphicon-chevron-right'; + }); + $scope.connectbutton = 'Connect'; + $scope.connectbuttonicon = 'glyphicon-chevron-right'; + + $scope.getBuffers = models.getBuffers.bind(models); + + $scope.bufferlines = {}; + $scope.nicklist = {}; + + $scope.activeBuffer = models.getActiveBuffer; + + $rootScope.connected = false; + $rootScope.waseverconnected = false; + $rootScope.userdisconnect = false; + $rootScope.reconnecting = false; + + $rootScope.models = models; + + $rootScope.iterCandidate = null; + + if (settings.savepassword) { + $scope.$watch('password', function() { + settings.password = $scope.password; + }); + settings.addCallback('password', function(password) { + $scope.password = password; + }); + $scope.password = settings.password; + } else { + settings.password = ''; + } + + // Check if user decides to save password, and copy it over + settings.addCallback('savepassword', function(newvalue) { + if (settings.savepassword) { + // Init value in settings module + settings.setDefaults({'password': $scope.password}); + settings.password = $scope.password; + } + }); + + $rootScope.wasMobileUi = false; + if (utils.isMobileUi()) { + $rootScope.wasMobileUi = true; + } + + if (!settings.fontfamily) { + if (utils.isMobileUi()) { + settings.fontfamily = 'sans-serif'; + } else { + settings.fontfamily = "Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace"; + } + } + + $scope.isSidebarVisible = function() { + return document.getElementById('content').getAttribute('sidebar-state') === 'visible'; + }; + + $scope.showSidebar = function() { + document.getElementById('sidebar').setAttribute('data-state', 'visible'); + document.getElementById('content').setAttribute('sidebar-state', 'visible'); + if (utils.isMobileUi()) { + // de-focus the input bar when opening the sidebar on mobile, so that the keyboard goes down + _.each(document.getElementsByTagName('textarea'), function(elem) { + $timeout(function(){elem.blur();}); + }); + } + }; + + $rootScope.hideSidebar = function() { + if (utils.isMobileUi()) { + document.getElementById('sidebar').setAttribute('data-state', 'hidden'); + document.getElementById('content').setAttribute('sidebar-state', 'hidden'); + } + }; + settings.addCallback('autoconnect', function(autoconnect) { + if (autoconnect && !$rootScope.connected && !$rootScope.sslError && !$rootScope.securityError && !$rootScope.errorMessage) { + $scope.connect(); + } + }); + + // toggle sidebar (if on mobile) + $scope.toggleSidebar = function() { + if (utils.isMobileUi()) { + if ($scope.isSidebarVisible()) { + $scope.hideSidebar(); + } else { + $scope.showSidebar(); + } + } + }; + + // Open and close panels while on mobile devices through swiping + $scope.openNick = function() { + if (utils.isMobileUi()) { + if (settings.nonicklist) { + settings.nonicklist = false; + } + } + }; + + $scope.closeNick = function() { + if (utils.isMobileUi()) { + if (!settings.nonicklist) { + settings.nonicklist = true; + } + } + }; + + // Watch model and update channel sorting when it changes + settings.addCallback('orderbyserver', function(orderbyserver) { + $rootScope.predicate = orderbyserver ? 'serverSortKey' : 'number'; + }); + + settings.addCallback('useFavico', function(useFavico) { + // this check is necessary as this is called on page load, too + if (!$rootScope.connected) { + return; + } + if (useFavico) { + notifications.updateFavico(); + } else { + $rootScope.favico.reset(); + } + }); + + // To prevent unnecessary loading times for users who don't + // want MathJax, load it only if the setting is enabled. + // This also fires when the page is loaded if enabled. + settings.addCallback('enableMathjax', function(enabled) { + if (enabled && !$rootScope.mathjax_init) { + // Load MathJax only once + $rootScope.mathjax_init = true; + (function () { + var head = document.getElementsByTagName("head")[0], script; + script = document.createElement("script"); + script.type = "text/x-mathjax-config"; + script[(window.opera ? "innerHTML" : "text")] = + "MathJax.Hub.Config({\n" + + " tex2jax: { inlineMath: [['$$','$$'], ['\\\\(','\\\\)']], displayMath: [['\\\\[','\\\\]']] },\n" + + "});"; + head.appendChild(script); + script = document.createElement("script"); + script.type = "text/javascript"; + script.src = "//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"; + head.appendChild(script); + })(); + } + }); + + + // Inject theme CSS + settings.addCallback('theme', function(theme) { + // Unload old theme + var oldThemeCSS = document.getElementById("themeCSS"); + if (oldThemeCSS) { + oldThemeCSS.parentNode.removeChild(oldThemeCSS); + } + + // Load new theme + (function() { + var elem = document.createElement("link"); + elem.rel = "stylesheet"; + elem.href = "css/themes/" + theme + ".css"; + elem.media = "screen"; + elem.id = "themeCSS"; + document.getElementsByTagName("head")[0].appendChild(elem); + })(); + }); + + settings.addCallback('customCSS', function(css) { + // We need to delete the old tag and add a new one so that the browser + // notices the change. Thus, first remove old custom CSS. + var old_css = document.getElementById('custom-css-tag'); + if (old_css) { + old_css.parentNode.removeChild(old_css); + } + + // Create new CSS tag + var new_css = document.createElement("style"); + new_css.type = "text/css"; + new_css.id = "custom-css-tag"; + new_css.appendChild(document.createTextNode(css)); + // Append it to the tag + var heads = document.getElementsByTagName("head"); + heads[0].appendChild(new_css); + }); + + + // Update font family when changed + settings.addCallback('fontfamily', function(fontfamily) { + utils.changeClassStyle('favorite-font', 'fontFamily', fontfamily); + }); + // Update font size when changed + settings.addCallback('fontsize', function(fontsize) { + utils.changeClassStyle('favorite-font', 'fontSize', fontsize); + }); + + $scope.setActiveBuffer = function(bufferId, key) { + // If we are on mobile we need to collapse the menu on sidebar clicks + // We use 968 px as the cutoff, which should match the value in glowingbear.css + if (utils.isMobileUi()) { + $scope.hideSidebar(); + } + + // Clear the hotlist for this buffer, because presumable you have read + // the messages in this buffer before you switched to the new one + // this is only needed with new type of clearing since in the old + // way WeeChat itself takes care of that part + if (models.version[0] >= 1) { + connection.sendHotlistClear(); + } + + return models.setActiveBuffer(bufferId, key); + }; + + $scope.openBuffer = function(bufferName) { + var fullName = models.getActiveBuffer().fullName; + fullName = fullName.substring(0, fullName.lastIndexOf('.') + 1) + bufferName; // substitute the last part + + if (!$scope.setActiveBuffer(fullName, 'fullName')) { + // WeeChat 0.4.0+ supports /join -noswitch + // As Glowing Bear requires 0.4.2+, we don't need to check the version + var command = 'join -noswitch'; + + // Check if it's a query and we need to use /query instead + if (['#', '&', '+', '!'].indexOf(bufferName.charAt(0)) < 0) { // these are the characters a channel name can start with (RFC 2813-2813) + command = 'query'; + // WeeChat 1.2+ supports /query -noswitch. See also #577 (different context) + if ((models.version[0] == 1 && models.version[1] >= 2) || models.version[1] > 1) { + command += " -noswitch"; + } + } + connection.sendMessage('/' + command + ' ' + bufferName); + } + }; + + +//XXX this does not belong here (or does it?) + // Calculate number of lines to fetch + $scope.calculateNumLines = function() { + var bufferlineElements = document.querySelectorAll(".bufferline"); + var lineHeight = 0, idx = 0; + while (lineHeight === 0 && idx < bufferlineElements.length) { + lineHeight = bufferlineElements[idx++].clientHeight; + } + var areaHeight = document.querySelector("#bufferlines").clientHeight; + // Fetch 10 lines more than theoretically needed so that scrolling up will correctly trigger the loading of more lines + // Also, some lines might be hidden, so it's probably better to have a bit of buffer there + var numLines = Math.ceil(areaHeight/lineHeight + 10); + $scope.lines_per_screen = numLines; + }; + $scope.calculateNumLines(); + + // get animationframe method + window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame; + + // Recalculate number of lines on resize + window.addEventListener("resize", _.debounce(function() { + // Recalculation fails when not connected + if ($rootScope.connected) { + // Show the sidebar if switching away from mobile view, hide it when switching to mobile + // Wrap in a condition so we save ourselves the $apply if nothing changes (50ms or more) + if ($scope.wasMobileUi && !utils.isMobileUi()) { + $scope.showSidebar(); + } + $scope.wasMobileUi = utils.isMobileUi(); + $scope.calculateNumLines(); + + // if we're scrolled to the bottom, scroll down to the same position after the resize + // most common use case: opening the keyboard on a mobile device + if ($rootScope.bufferBottom) { + var rescroll = function(){ + $rootScope.updateBufferBottom(true); + }; + $timeout(rescroll, 500); + window.requestAnimationFrame(rescroll); + } + } + }, 100)); + + $rootScope.loadingLines = false; + $scope.fetchMoreLines = function(numLines) { + if (!numLines) { + numLines = $scope.lines_per_screen; + } + return connection.fetchMoreLines(numLines); + }; + + $scope.infiniteScroll = function() { + // Check if we are already fetching + if ($rootScope.loadingLines) { + return; + } + var buffer = models.getActiveBuffer(); + if (!buffer.allLinesFetched) { + $scope.fetchMoreLines(); + } + }; + + $rootScope.updateBufferBottom = function(bottom) { + var eob = document.getElementById("end-of-buffer"); + var bl = document.getElementById('bufferlines'); + if (bottom) { + eob.scrollIntoView(); + } + $rootScope.bufferBottom = eob.offsetTop <= bl.scrollTop + bl.clientHeight; + }; + $rootScope.scrollWithBuffer = function(scrollToReadmarker, moreLines) { + // First, get scrolling status *before* modification + // This is required to determine where we were in the buffer pre-change + var bl = document.getElementById('bufferlines'); + var sVal = bl.scrollHeight - bl.clientHeight; + + var scroll = function() { + var sTop = bl.scrollTop; + // Determine if we want to scroll at all + // Give the check 3 pixels of slack so you don't have to hit + // the exact spot. This fixes a bug in some browsers + if (((scrollToReadmarker || moreLines) && sTop < sVal) || (Math.abs(sTop - sVal) < 3)) { + var readmarker = document.querySelector(".readmarker"); + if (scrollToReadmarker && readmarker) { + // Switching channels, scroll to read marker + bl.scrollTop = readmarker.offsetTop - readmarker.parentElement.scrollHeight + readmarker.scrollHeight; + } else if (moreLines) { + // We fetched more lines but the read marker is still out of view + // Keep the scroll position constant + bl.scrollTop = bl.scrollHeight - bl.clientHeight - sVal; + } else { + // New message, scroll with buffer (i.e. to bottom) + var eob = document.getElementById("end-of-buffer"); + eob.scrollIntoView(); + } + $rootScope.updateBufferBottom(); + } + }; + // Here be scrolling dragons + $timeout(scroll); + window.requestAnimationFrame(scroll); + }; + + + $scope.connect = function() { + notifications.requestNotificationPermission(); + $rootScope.sslError = false; + $rootScope.securityError = false; + $rootScope.errorMessage = false; + $rootScope.bufferBottom = true; + $scope.connectbutton = 'Connecting'; + $scope.connectbuttonicon = 'glyphicon-refresh glyphicon-spin'; + connection.connect(settings.host, settings.port, $scope.password, settings.ssl); + }; + $scope.disconnect = function() { + $scope.connectbutton = 'Connect'; + $scope.connectbuttonicon = 'glyphicon-chevron-right'; + connection.disconnect(); + }; + $scope.reconnect = function() { + var bufferId = models.getActiveBuffer().id; + connection.attemptReconnect(bufferId, 3000); + }; + +//XXX this is a bit out of place here, either move up to the rest of the firefox install code or remove + $scope.install = function() { + if (navigator.mozApps !== undefined) { + // Find absolute url with trailing '/' or '/index.html' removed + var base_url = location.protocol + '//' + location.host + + location.pathname.replace(/\/(index\.html)?$/, ''); + var request = navigator.mozApps.install(base_url + '/manifest.webapp'); + request.onsuccess = function () { + $scope.isinstalled = true; + // Save the App object that is returned + var appRecord = this.result; + // Start the app. + appRecord.launch(); + alert('Installation successful!'); + }; + request.onerror = function () { + // Display the error information from the DOMError object + alert('Install failed, error: ' + this.error.name); + }; + } else { + alert('Sorry. Only supported in Firefox v26+'); + } + }; + + $scope.showModal = function(elementId) { + document.getElementById(elementId).setAttribute('data-state', 'visible'); + }; + $scope.closeModal = function($event) { + function closest(elem, selector) { + var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector; + while (elem) { + if (matchesSelector.call(elem, selector)) return elem; + else elem = elem.parentElement; + } + } + closest($event.target, '.gb-modal').setAttribute('data-state', 'hidden'); + }; + + $scope.toggleAccordion = function(event) { + event.stopPropagation(); + event.preventDefault(); + + var target = event.target.parentNode.parentNode.parentNode; + target.setAttribute('data-state', target.getAttribute('data-state') === 'active' ? 'collapsed' : 'active'); + + // Hide all other siblings + var siblings = target.parentNode.children; + for (var childId in siblings) { + var child = siblings[childId]; + if (child.nodeType === 1 && child !== target) { + child.setAttribute('data-state', 'collapsed'); + } + } + }; + +//XXX what do we do with this? + $scope.hasUnread = function(buffer) { + // if search is set, return every buffer + if ($scope.search && $scope.search !== "") { + return true; + } + if (settings.onlyUnread) { + // Always show current buffer in list + if (models.getActiveBuffer() === buffer) { + return true; + } + // Always show core buffer in the list (issue #438) + // Also show server buffers in hierarchical view + if (buffer.fullName === "core.weechat" || (settings.orderbyserver && buffer.type === 'server')) { + return true; + } + return (buffer.unread > 0 || buffer.notification > 0) && !buffer.hidden; + } + return !buffer.hidden; + }; + + // Watch model and update show setting when it changes + settings.addCallback('nonicklist', function() { + $scope.showNicklist = $scope.updateShowNicklist(); + // restore bottom view + if ($rootScope.connected && $rootScope.bufferBottom) { + $timeout(function(){ + $rootScope.updateBufferBottom(true); + }, 500); + } + }); + $scope.showNicklist = false; + // Utility function that template can use to check if nicklist should + // be displayed for current buffer or not + // is called on buffer switch + $scope.updateShowNicklist = function() { + var ab = models.getActiveBuffer(); + if (!ab) { + return false; + } + // Check if option no nicklist is set + if (settings.nonicklist) { + return false; + } + // Check if nicklist is empty + if (ab.isNicklistEmpty()) { + return false; + } + return true; + }; + +//XXX not sure whether this belongs here + $rootScope.switchToActivityBuffer = function() { + // Find next buffer with activity and switch to it + var sortedBuffers = _.sortBy($scope.getBuffers(), 'number'); + var i, buffer; + // Try to find buffer with notification + for (i in sortedBuffers) { + buffer = sortedBuffers[i]; + if (buffer.notification > 0) { + $scope.setActiveBuffer(buffer.id); + return; // return instead of break so that the second for loop isn't executed + } + } + // No notifications, find first buffer with unread lines instead + for (i in sortedBuffers) { + buffer = sortedBuffers[i]; + if (buffer.unread > 0) { + $scope.setActiveBuffer(buffer.id); + return; + } + } + }; + // Helper function since the keypress handler is in a different scope + $rootScope.toggleNicklist = function() { + settings.nonicklist = !settings.nonicklist; + }; + + $rootScope.switchToAdjacentBuffer = function(direction) { + // direction is +1 for next buffer, -1 for previous buffer + var sortedBuffers = _.sortBy($scope.getBuffers(), $rootScope.predicate); + var activeBuffer = models.getActiveBuffer(); + var index = sortedBuffers.indexOf(activeBuffer); + if (index >= 0) { + var newBuffer = sortedBuffers[index + direction]; + if (newBuffer) { + $scope.setActiveBuffer(newBuffer.id); + } + } + }; + + $scope.handleSearchBoxKey = function($event) { + // Support different browser quirks + var code = $event.keyCode ? $event.keyCode : $event.charCode; + // Handle escape + if (code === 27) { + $event.preventDefault(); + $scope.search = ''; + } // Handle enter + else if (code === 13) { + $event.preventDefault(); + if ($scope.filteredBuffers.length > 0) { + $scope.setActiveBuffer($scope.filteredBuffers[0].id); + } + $scope.search = ''; + } + }; + + $rootScope.supports_formatting_date = (function() { + // function toLocaleDateStringSupportsLocales taken from MDN: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString#Checking_for_support_for_locales_and_options_arguments + try { + new Date().toLocaleDateString('i'); + } catch (e) { + if (e.name !== 'RangeError') { + $log.info("Browser does not support toLocaleDateString()," + + " falling back to en-US"); + } + return e.name === 'RangeError'; + } + $log.info("Browser does not support toLocaleDateString()," + + " falling back to en-US"); + return false; + })(); + + // Prevent user from accidentally leaving the page + window.onbeforeunload = function(event) { + + if ($scope.command !== null && $scope.command !== '') { + event.preventDefault(); + // Chrome requires this + // Firefox does not show the site provides message + event.returnValue = "Any unsent input will be lost. Are you sure that you want to quit?"; + + } else { + if ($rootScope.connected) { + $scope.disconnect(); + } + $scope.favico.reset(); + } + }; + + $scope.init = function() { + if (window.location.hash) { + var rawStr = atob(window.location.hash.substring(1)); + window.location.hash = ""; + var spl = rawStr.split(":"); + var host = spl[0]; + var port = parseInt(spl[1]); + var password = spl[2]; + var ssl = spl.length > 3; + notifications.requestNotificationPermission(); + $rootScope.sslError = false; + $rootScope.securityError = false; + $rootScope.errorMessage = false; + $rootScope.bufferBottom = true; + $scope.connectbutton = 'Connecting'; + $scope.connectbuttonicon = 'glyphicon-chevron-right'; + connection.connect(host, port, password, ssl); + } + }; + +}]); + +weechat.config(['$routeProvider', + function($routeProvider) { + $routeProvider.when('/', { + templateUrl: 'index.html', + controller: 'WeechatCtrl' + }); + } +]); + +})(); diff --git a/sources/js/handlers.js b/sources/js/handlers.js new file mode 100644 index 0000000..c1b3228 --- /dev/null +++ b/sources/js/handlers.js @@ -0,0 +1,451 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat'); + +weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notifications', function($rootScope, $log, models, plugins, notifications) { + + var handleVersionInfo = function(message) { + var content = message.objects[0].content; + var version = content.value; + // Store the WeeChat version in models + // this eats things like 1.3-dev -> [1,3] + models.version = version.split(".").map(function(c) { return parseInt(c); }); + }; + + var handleConfValue = function(message) { + var infolist = message.objects[0].content; + for (var i = 0; i < infolist.length ; i++) { + var key, val; + var item = infolist[i]; + for (var j = 0; j < item.length ; j++) { + var confitem = item[j]; + if (confitem.full_name) { + key = confitem.full_name; + } + if (confitem.value) { + val = confitem.value; + } + } + if (key && val) { + $log.debug('Setting wconfig "' + key + '" to value "' + val + '"'); + models.wconfig[key] = val; + } + } + }; + + var handleBufferClosing = function(message) { + var bufferMessage = message.objects[0].content[0]; + var bufferId = bufferMessage.pointers[0]; + models.closeBuffer(bufferId); + }; + + // inject a fake buffer line for date change if needed + var injectDateChangeMessageIfNeeded = function(buffer, manually, old_date, new_date) { + if (buffer.bufferType === 1) { + // Don't add date change messages to free buffers + return; + } + old_date.setHours(0, 0, 0, 0); + new_date.setHours(0, 0, 0, 0); + // Check if the date changed + if (old_date.valueOf() !== new_date.valueOf()) { + if (manually) { + // if the message that caused this date change to be sent + // would increment buffer.lastSeen, we should increment as + // well. + ++buffer.lastSeen; + } + var old_date_plus_one = old_date; + old_date_plus_one.setDate(old_date.getDate() + 1); + // it's not always true that a date with time 00:00:00 + // plus one day will be time 00:00:00 + old_date_plus_one.setHours(0, 0, 0, 0); + + var content = "\u001943"; // this colour corresponds to chat_day_change + // Add day of the week + if ($rootScope.supports_formatting_date) { + content += new_date.toLocaleDateString(window.navigator.language, + {weekday: "long"}); + } else { + // Gross code that only does English dates ew gross + var dow_to_word = [ + "Sunday", "Monday", "Tuesday", + "Wednesday", "Thursday", "Friday", "Saturday"]; + content += dow_to_word[new_date.getDay()]; + } + // if you're testing different date formats, + // make sure to test different locales such as "en-US", + // "en-US-u-ca-persian" (which has different weekdays, year 0, and an ERA) + // "ja-JP-u-ca-persian-n-thai" (above, diff numbering, diff text) + var extra_date_format = { + day: "numeric", + month: "long" + }; + if (new_date.getYear() !== old_date.getYear()) { + extra_date_format.year = "numeric"; + } + content += " ("; + if ($rootScope.supports_formatting_date) { + content += new_date.toLocaleDateString(window.navigator.language, + extra_date_format); + } else { + // ew ew not more gross code + var month_to_word = [ + "January", "February", "March", "April", + "May", "June", "July", "August", + "September", "October", "November", "December"]; + content += month_to_word[new_date.getMonth()] + " " + new_date.getDate().toString(); + if (extra_date_format.year === "numeric") { + content += ", " + new_date.getFullYear().toString(); + } + } + // Result should be something like + // Friday (November 27) + // or if the year is different, + // Friday (November 27, 2015) + + // Comparing dates in javascript is beyond tedious + if (old_date_plus_one.valueOf() !== new_date.valueOf()) { + var date_diff = Math.round((new_date - old_date)/(24*60*60*1000)) + 1; + if (date_diff < 0) { + date_diff = -1*(date_diff); + if (date_diff === 1) { + content += ", 1 day before"; + } else { + content += ", " + date_diff + " days before"; + } + } else { + content += ", " + date_diff + " days later"; + } + // Result: Friday (November 27, 5 days later) + } + content += ")"; + + var line = { + buffer: buffer.id, + date: new_date, + prefix: '\u001943\u2500', + tags_array: [], + displayed: true, + highlight: 0, + message: content + }; + var new_message = new models.BufferLine(line); + buffer.addLine(new_message); + } + }; + + var handleLine = function(line, manually) { + var message = new models.BufferLine(line); + var buffer = models.getBuffer(message.buffer); + buffer.requestedLines++; + // Only react to line if its displayed + if (message.displayed) { + // Check for date change + if (buffer.lines.length > 0) { + var old_date = new Date(buffer.lines[buffer.lines.length - 1].date), + new_date = new Date(message.date); + injectDateChangeMessageIfNeeded(buffer, manually, old_date, new_date); + } + + message = plugins.PluginManager.contentForMessage(message); + buffer.addLine(message); + + if (manually) { + buffer.lastSeen++; + } + + if (buffer.active && !manually) { + $rootScope.scrollWithBuffer(); + } + + if (!manually && (!buffer.active || !$rootScope.isWindowFocused())) { + if (buffer.notify > 1 && _.contains(message.tags, 'notify_message') && !_.contains(message.tags, 'notify_none')) { + buffer.unread++; + $rootScope.$emit('notificationChanged'); + } + + if ((buffer.notify !== 0 && message.highlight) || _.contains(message.tags, 'notify_private')) { + buffer.notification++; + notifications.createHighlight(buffer, message); + $rootScope.$emit('notificationChanged'); + } + } + } + }; + + var handleBufferInfo = function(message) { + var bufferInfos = message.objects[0].content; + // buffers objects + for (var i = 0; i < bufferInfos.length ; i++) { + var bufferId = bufferInfos[i].pointers[0]; + var buffer = models.getBuffer(bufferId); + if (buffer !== undefined) { + // We already know this buffer + handleBufferUpdate(buffer, bufferInfos[i]); + } else { + buffer = new models.Buffer(bufferInfos[i]); + models.addBuffer(buffer); + // Switch to first buffer on startup + if (i === 0) { + models.setActiveBuffer(buffer.id); + } + } + } + }; + + var handleBufferUpdate = function(buffer, message) { + if (message.pointers[0] !== buffer.id) { + // this is information about some other buffer! + return; + } + + // weechat properties -- short name can be changed + buffer.shortName = message.short_name; + buffer.trimmedName = buffer.shortName.replace(/^[#&+]/, ''); + buffer.title = message.title; + buffer.number = message.number; + buffer.hidden = message.hidden; + + // reset these, hotlist info will arrive shortly + buffer.notification = 0; + buffer.unread = 0; + buffer.lastSeen = -1; + + if (message.local_variables.type !== undefined) { + buffer.type = message.local_variables.type; + buffer.indent = (['channel', 'private'].indexOf(buffer.type) >= 0); + } + + if (message.notify !== undefined) { + buffer.notify = message.notify; + } + }; + + var handleBufferLineAdded = function(message) { + message.objects[0].content.forEach(function(l) { + handleLine(l, false); + }); + }; + + var handleBufferOpened = function(message) { + var bufferMessage = message.objects[0].content[0]; + var buffer = new models.Buffer(bufferMessage); + models.addBuffer(buffer); + }; + + var handleBufferTitleChanged = function(message) { + var obj = message.objects[0].content[0]; + var buffer = obj.pointers[0]; + var old = models.getBuffer(buffer); + old.fullName = obj.full_name; + old.title = models.parseRichText(obj.title); + old.number = obj.number; + + old.rtitle = ""; + for (var i = 0; i < old.title.length; ++i) { + old.rtitle += old.title[i].text; + } + }; + + var handleBufferRenamed = function(message) { + var obj = message.objects[0].content[0]; + var buffer = obj.pointers[0]; + var old = models.getBuffer(buffer); + old.fullName = obj.full_name; + old.shortName = obj.short_name; + // If it's a channel, trim away the prefix (#, &, or +). If that is empty and the buffer + // has a short name, use a space (because the prefix will be displayed separately, and we don't want + // prefix + fullname, which would happen otherwise). Else, use null so that full_name is used + old.trimmedName = obj.short_name.replace(/^[#&+]/, '') || (obj.short_name ? ' ' : null); + old.prefix = ['#', '&', '+'].indexOf(obj.short_name.charAt(0)) >= 0 ? obj.short_name.charAt(0) : ''; + + // After a buffer openes we get the name change event from relay protocol + // Here we check our outgoing commands that openes a buffer and switch + // to it if we find the buffer name it the list + var position = models.outgoingQueries.indexOf(old.shortName); + if (position >= 0) { + models.outgoingQueries.splice(position, 1); + models.setActiveBuffer(old.id); + } + }; + + var handleBufferHidden = function(message) { + var obj = message.objects[0].content[0]; + var buffer = obj.pointers[0]; + var old = models.getBuffer(buffer); + old.hidden = true; + }; + + var handleBufferUnhidden = function(message) { + var obj = message.objects[0].content[0]; + var buffer = obj.pointers[0]; + var old = models.getBuffer(buffer); + old.hidden = false; + }; + + var handleBufferLocalvarChanged = function(message) { + var obj = message.objects[0].content[0]; + var buffer = obj.pointers[0]; + var old = models.getBuffer(buffer); + + var localvars = obj.local_variables; + if (old !== undefined && localvars !== undefined) { + // Update indentation status + old.type = localvars.type; + old.indent = (['channel', 'private'].indexOf(localvars.type) >= 0); + // Update serverSortKey and related variables + old.plugin = localvars.plugin; + old.server = localvars.server; + old.serverSortKey = old.plugin + "." + old.server + + (old.type === "server" ? "" : ("." + old.shortName)); + } + }; + + var handleBufferTypeChanged = function(message) { + var obj = message.objects[0].content[0]; + var buffer = obj.pointers[0]; + var old = models.getBuffer(buffer); + // 0 = formatted (normal); 1 = free + buffer.bufferType = obj.type; + }; + + /* + * Handle answers to (lineinfo) messages + * + * (lineinfo) messages are specified by this client. It is request after bufinfo completes + */ + var handleLineInfo = function(message, manually) { + var lines = message.objects[0].content.reverse(); + if (manually === undefined) { + manually = true; + } + lines.forEach(function(l) { + handleLine(l, manually); + }); + if (message.objects[0].content.length > 0) { + // fiddle out the buffer ID and take the last line's date + var last_line = + message.objects[0].content[message.objects[0].content.length-1]; + var buffer = models.getBuffer(last_line.buffer); + if (buffer.lines.length > 0) { + var last_date = new Date(buffer.lines[buffer.lines.length - 1].date); + injectDateChangeMessageIfNeeded(buffer, true, last_date, new Date()); + } + } + }; + + /* + * Handle answers to hotlist request + */ + var handleHotlistInfo = function(message) { + if (message.objects.length === 0) { + return; + } + var hotlist = message.objects[0].content; + hotlist.forEach(function(l) { + var buffer = models.getBuffer(l.buffer); + // 1 is message + buffer.unread += l.count[1]; + // 2 is private + buffer.notification += l.count[2]; + // 3 is highlight + buffer.notification += l.count[3]; + /* Since there is unread messages, we can guess + * what the last read line is and update it accordingly + */ + var unreadSum = _.reduce(l.count, function(memo, num) { return memo + num; }, 0); + buffer.lastSeen = buffer.lines.length - 1 - unreadSum; + }); + }; + + /* + * Handle nicklist event + */ + var handleNicklist = function(message) { + var nicklist = message.objects[0].content; + var group = 'root'; + nicklist.forEach(function(n) { + var buffer = models.getBuffer(n.pointers[0]); + if (n.group === 1) { + var g = new models.NickGroup(n); + group = g.name; + buffer.nicklist[group] = g; + } else { + var nick = new models.Nick(n); + buffer.addNick(group, nick); + } + }); + }; + /* + * Handle nicklist diff event + */ + var handleNicklistDiff = function(message) { + var nicklist = message.objects[0].content; + var group; + nicklist.forEach(function(n) { + var buffer = models.getBuffer(n.pointers[0]); + var d = n._diff; + if (n.group === 1) { + group = n.name; + if (group === undefined) { + var g = new models.NickGroup(n); + buffer.nicklist[group] = g; + group = g.name; + } + } else { + var nick = new models.Nick(n); + if (d === 43) { // + + buffer.addNick(group, nick); + } else if (d === 45) { // - + buffer.delNick(group, nick); + } else if (d === 42) { // * + buffer.updateNick(group, nick); + } + } + }); + }; + + var eventHandlers = { + _buffer_closing: handleBufferClosing, + _buffer_line_added: handleBufferLineAdded, + _buffer_localvar_added: handleBufferLocalvarChanged, + _buffer_localvar_removed: handleBufferLocalvarChanged, + _buffer_localvar_changed: handleBufferLocalvarChanged, + _buffer_opened: handleBufferOpened, + _buffer_title_changed: handleBufferTitleChanged, + _buffer_type_changed: handleBufferTypeChanged, + _buffer_renamed: handleBufferRenamed, + _buffer_hidden: handleBufferHidden, + _buffer_unhidden: handleBufferUnhidden, + _nicklist: handleNicklist, + _nicklist_diff: handleNicklistDiff + }; + + $rootScope.$on('onMessage', function(event, message) { + if (_.has(eventHandlers, message.id)) { + eventHandlers[message.id](message); + } else { + $log.debug('Unhandled event received: ' + message.id); + } + }); + + var handleEvent = function(event) { + if (_.has(eventHandlers, event.id)) { + eventHandlers[event.id](event); + } + }; + + return { + handleVersionInfo: handleVersionInfo, + handleConfValue: handleConfValue, + handleEvent: handleEvent, + handleLineInfo: handleLineInfo, + handleHotlistInfo: handleHotlistInfo, + handleNicklist: handleNicklist, + handleBufferInfo: handleBufferInfo + }; + +}]); +})(); diff --git a/sources/js/imgur-drop-directive.js b/sources/js/imgur-drop-directive.js new file mode 100644 index 0000000..7e114d0 --- /dev/null +++ b/sources/js/imgur-drop-directive.js @@ -0,0 +1,49 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat'); + +weechat.directive('imgurDrop', ['connection','imgur','$rootScope', function(connection, imgur, $rootScope) { + return { + restrict: 'A', + link: function($scope, element, attr) { + var elem = element[0]; + elem.ondragover = function () { this.classList.add('imgur-drop-hover'); return false; }; + elem.ondragend = function () { this.classList.remove('imgur-drop-hover'); return false; }; + elem.ondrop = function(e) { + // Remove hover class + this.classList.remove('imgur-drop-hover'); + + // Get files + var files = e.dataTransfer.files; + + // Stop default behaviour + e.stopPropagation(); + e.preventDefault(); + + // Send image url after upload + var sendImageUrl = function(imageUrl) { + + // Send image + if(imageUrl !== undefined && imageUrl !== '') { + $rootScope.insertAtCaret(String(imageUrl)); + } + + }; + + // Check files + if(typeof files !== "undefined" && files.length > 0) { + + // Loop through files + for (var i = 0; i < files.length; i++) { + // Upload to imgur + imgur.process(files[i], sendImageUrl); + } + + } + }; + } + }; +}]); + +})(); diff --git a/sources/js/imgur.js b/sources/js/imgur.js new file mode 100644 index 0000000..a5a0fd0 --- /dev/null +++ b/sources/js/imgur.js @@ -0,0 +1,128 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat'); + +weechat.factory('imgur', ['$rootScope', function($rootScope) { + + var process = function(image, callback) { + + // Is it an image? + if (!image || !image.type.match(/image.*/)) return; + + // New file reader + var reader = new FileReader(); + + // When image is read + reader.onload = function (event) { + var image = event.target.result.split(',')[1]; + upload(image, callback); + }; + + // Read image as data url + reader.readAsDataURL(image); + + }; + + // Upload image to imgur from base64 + var upload = function( base64img, callback ) { + // Set client ID (Glowing Bear) + var clientId = "164efef8979cd4b"; + + // Progress bars container + var progressBars = document.getElementById("imgur-upload-progress"), + currentProgressBar = document.createElement("div"); + + // Set progress bar attributes + currentProgressBar.className='imgur-progress-bar'; + currentProgressBar.style.width = '0'; + + // Append progress bar + progressBars.appendChild(currentProgressBar); + + // Create new form data + var fd = new FormData(); + fd.append("image", base64img); // Append the file + fd.append("type", "base64"); // Set image type to base64 + + // Create new XMLHttpRequest + var xhttp = new XMLHttpRequest(); + + // Post request to imgur api + xhttp.open("POST", "https://api.imgur.com/3/image", true); + + // Set headers + xhttp.setRequestHeader("Authorization", "Client-ID " + clientId); + xhttp.setRequestHeader("Accept", "application/json"); + + // Handler for response + xhttp.onload = function() { + + // Remove progress bar + currentProgressBar.parentNode.removeChild(currentProgressBar); + + // Check state and response status + if(xhttp.status === 200) { + + // Get response text + var response = JSON.parse(xhttp.responseText); + + // Send link as message + if( response.data && response.data.link ) { + + if (callback && typeof(callback) === "function") { + callback(response.data.link); + } + + } else { + showErrorMsg(); + } + + } else { + showErrorMsg(); + } + + }; + + if( "upload" in xhttp ) { + + // Set progress + xhttp.upload.onprogress = function (event) { + + // Check if we can compute progress + if (event.lengthComputable) { + // Complete in percent + var complete = (event.loaded / event.total * 100 | 0); + + // Set progress bar width + currentProgressBar.style.width = complete + '%'; + } + }; + + } + + // Send request with form data + xhttp.send(fd); + + }; + + var showErrorMsg = function() { + // Show error msg + $rootScope.uploadError = true; + $rootScope.$apply(); + + // Hide after 5 seconds + setTimeout(function(){ + // Hide error msg + $rootScope.uploadError = false; + $rootScope.$apply(); + }, 5000); + }; + + return { + process: process + }; + +}]); + +})(); diff --git a/sources/js/inputbar.js b/sources/js/inputbar.js new file mode 100644 index 0000000..2cd24c7 --- /dev/null +++ b/sources/js/inputbar.js @@ -0,0 +1,489 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat'); + +weechat.directive('inputBar', function() { + + return { + + templateUrl: 'directives/input.html', + + scope: { + inputId: '@inputId', + command: '=command' + }, + + controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'imgur', 'models', 'IrcUtils', 'settings', function($rootScope, + $scope, + $element, //XXX do we need this? don't seem to be using it + $log, + connection, //XXX we should eliminate this dependency and use signals instead + imgur, + models, + IrcUtils, + settings) { + + // E.g. Turn :smile: into the unicode equivalent + $scope.inputChanged = function() { + $scope.command = emojione.shortnameToUnicode($scope.command); + }; + + /* + * Returns the input element + */ + $scope.getInputNode = function() { + return document.querySelector('textarea#' + $scope.inputId); + }; + + $scope.hideSidebar = function() { + $rootScope.hideSidebar(); + }; + + $scope.completeNick = function() { + // input DOM node + var inputNode = $scope.getInputNode(); + + // get current caret position + var caretPos = inputNode.selectionStart; + + // get current active buffer + var activeBuffer = models.getActiveBuffer(); + + // Empty input makes $scope.command undefined -- use empty string instead + var input = $scope.command || ''; + + // complete nick + var nickComp = IrcUtils.completeNick(input, caretPos, $scope.iterCandidate, + activeBuffer.getNicklistByTime(), ':'); + + // remember iteration candidate + $scope.iterCandidate = nickComp.iterCandidate; + + // update current input + $scope.command = nickComp.text; + + // update current caret position + setTimeout(function() { + inputNode.focus(); + inputNode.setSelectionRange(nickComp.caretPos, nickComp.caretPos); + }, 0); + }; + + $rootScope.insertAtCaret = function(toInsert) { + // caret position in the input bar + var inputNode = $scope.getInputNode(), + caretPos = inputNode.selectionStart; + + var prefix = $scope.command.substring(0, caretPos), + suffix = $scope.command.substring(caretPos, $scope.command.length); + // Add spaces if missing + if (prefix.length > 0 && prefix[prefix.length - 1] !== ' ') { + prefix += ' '; + } + if (suffix.length > 0 && suffix[0] !== ' ') { + suffix = ' '.concat(suffix); + } + $scope.command = prefix + toInsert + suffix; + + setTimeout(function() { + inputNode.focus(); + var pos = $scope.command.length - suffix.length; + inputNode.setSelectionRange(pos, pos); + // force refresh? + $scope.$apply(); + }, 0); + }; + + $scope.uploadImage = function($event, files) { + // Send image url after upload + var sendImageUrl = function(imageUrl) { + // Send image + if(imageUrl !== undefined && imageUrl !== '') { + $rootScope.insertAtCaret(String(imageUrl)); + } + }; + + if(typeof files !== "undefined" && files.length > 0) { + // Loop through files + for (var i = 0; i < files.length; i++) { + // Process image + imgur.process(files[i], sendImageUrl); + } + + } + }; + + // Send the message to the websocket + $scope.sendMessage = function() { + //XXX Use a signal here + var ab = models.getActiveBuffer(); + + // It's undefined early in the lifecycle of the program. + // Don't send empty commands + if($scope.command !== undefined && $scope.command !== '') { + + // log to buffer history + ab.addToHistory($scope.command); + + // Split the command into multiple commands based on line breaks + _.each($scope.command.split(/\r?\n/), function(line) { + // Ask before a /quit + if (line === '/quit' || line.indexOf('/quit ') === 0) { + if (!window.confirm("Are you sure you want to quit WeeChat? This will prevent you from connecting with Glowing Bear until you restart WeeChat on the command line!")) { + // skip this line + return; + } + } + connection.sendMessage(line); + }); + + // Check for /clear command + if ($scope.command === '/buffer clear' || $scope.command === '/c') { + $log.debug('Clearing lines'); + ab.clear(); + } + + // Check against a list of commands that opens a new + // buffer and save the name of the buffer so we can + // also automatically switch to the new buffer in gb + var opencommands = ['/query', '/join', '/j', '/q']; + var spacepos = $scope.command.indexOf(' '); + var firstword = $scope.command.substr(0, spacepos); + var index = opencommands.indexOf(firstword); + if (index >= 0) { + var queryName = $scope.command.substring(spacepos + 1); + // Cache our queries so when a buffer gets opened we can open in UI + models.outgoingQueries.push(queryName); + } + + // Empty the input after it's sent + $scope.command = ''; + } + + // New style clearing requires this, old does not + if (models.version[0] >= 1) { + connection.sendHotlistClear(); + } + + $scope.getInputNode().focus(); + }; + + //XXX THIS DOES NOT BELONG HERE! + $rootScope.addMention = function(prefix) { + // Extract nick from bufferline prefix + var nick = prefix[prefix.length - 1].text; + + var newValue = $scope.command || ''; // can be undefined, in that case, use the empty string + var addColon = newValue.length === 0; + if (newValue.length > 0) { + // Try to determine if it's a sequence of nicks + var trimmedValue = newValue.trim(); + if (trimmedValue.charAt(trimmedValue.length - 1) === ':') { + // get last word + var lastSpace = trimmedValue.lastIndexOf(' ') + 1; + var lastWord = trimmedValue.slice(lastSpace, trimmedValue.length - 1); + var nicklist = models.getActiveBuffer().getNicklistByTime(); + // check against nicklist to see if it's a list of highlights + for (var index in nicklist) { + if (nicklist[index].name === lastWord) { + // It's another highlight! + newValue = newValue.slice(0, newValue.lastIndexOf(':')) + ' '; + addColon = true; + break; + } + } + } + + // Add a space before the nick if there isn't one already + // Last char might have changed above, so re-check + if (newValue.charAt(newValue.length - 1) !== ' ') { + newValue += ' '; + } + } + // Add highlight to nicklist + newValue += nick; + if (addColon) { + newValue += ': '; + } + $scope.command = newValue; + $scope.getInputNode().focus(); + }; + + + // Handle key presses in the input bar + $rootScope.handleKeyPress = function($event) { + // don't do anything if not connected + if (!$rootScope.connected) { + return true; + } + + var inputNode = $scope.getInputNode(); + + // Support different browser quirks + var code = $event.keyCode ? $event.keyCode : $event.charCode; + + // Safari doesn't implement DOM 3 input events yet as of 8.0.6 + var altg = $event.getModifierState ? $event.getModifierState('AltGraph') : false; + + // Mac OSX behaves differntly for altgr, so we check for that + if (altg) { + // We don't handle any anything with altgr + return false; + } + + // reset quick keys display + $rootScope.showQuickKeys = false; + + // any other key than Tab resets nick completion iteration + var tmpIterCandidate = $scope.iterCandidate; + $scope.iterCandidate = null; + + // Left Alt+[0-9] -> jump to buffer + if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58)) { + if (code === 48) { + code = 58; + } + var bufferNumber = code - 48 - 1 ; + + var activeBufferId; + // quick select filtered entries + if (($scope.$parent.search.length || $scope.$parent.onlyUnread) && $scope.$parent.filteredBuffers.length) { + var filteredBufferNum = $scope.$parent.filteredBuffers[bufferNumber]; + if (filteredBufferNum !== undefined) { + activeBufferId = [filteredBufferNum.number, filteredBufferNum.id]; + } + } else { + // Map the buffers to only their numbers and IDs so we don't have to + // copy the entire (possibly very large) buffer object, and then sort + // the buffers according to their WeeChat number + var sortedBuffers = _.map(models.getBuffers(), function(buffer) { + return [buffer.number, buffer.id]; + }).sort(function(left, right) { + // By default, Array.prototype.sort() sorts alphabetically. + // Pass an ordering function to sort by first element. + return left[0] - right[0]; + }); + activeBufferId = sortedBuffers[bufferNumber]; + } + if (activeBufferId) { + $scope.$parent.setActiveBuffer(activeBufferId[1]); + $event.preventDefault(); + } + } + + // Tab -> nick completion + if (code === 9 && !$event.altKey && !$event.ctrlKey) { + $event.preventDefault(); + $scope.iterCandidate = tmpIterCandidate; + $scope.completeNick(); + return true; + } + + // Left Alt+n -> toggle nicklist + if ($event.altKey && !$event.ctrlKey && code === 78) { + $event.preventDefault(); + $rootScope.toggleNicklist(); + return true; + } + + // Alt+A -> switch to buffer with activity + if ($event.altKey && (code === 97 || code === 65)) { + $event.preventDefault(); + $rootScope.switchToActivityBuffer(); + return true; + } + + // Alt+Arrow up/down -> switch to prev/next adjacent buffer + if ($event.altKey && !$event.ctrlKey && (code === 38 || code === 40)) { + $event.preventDefault(); + var direction = code - 39; + $rootScope.switchToAdjacentBuffer(direction); + return true; + } + + // Alt+L -> focus on input bar + if ($event.altKey && (code === 76 || code === 108)) { + $event.preventDefault(); + inputNode.focus(); + inputNode.setSelectionRange($scope.command.length, $scope.command.length); + return true; + } + + // Alt+< -> switch to previous buffer + if ($event.altKey && (code === 60 || code === 226)) { + var previousBuffer = models.getPreviousBuffer(); + if (previousBuffer) { + models.setActiveBuffer(previousBuffer.id); + $event.preventDefault(); + return true; + } + } + + // Double-tap Escape -> disconnect + if (code === 27) { + $event.preventDefault(); + + // Check if a modal is visible. If so, close it instead of disconnecting + var modals = document.querySelectorAll('.gb-modal'); + for (var modalId = 0; modalId < modals.length; modalId++) { + if (modals[modalId].getAttribute('data-state') === 'visible') { + modals[modalId].setAttribute('data-state', 'hidden'); + return true; + } + } + + if (typeof $scope.lastEscape !== "undefined" && (Date.now() - $scope.lastEscape) <= 500) { + // Double-tap + connection.disconnect(); + } + $scope.lastEscape = Date.now(); + return true; + } + + // Alt+G -> focus on buffer filter input + if ($event.altKey && (code === 103 || code === 71)) { + $event.preventDefault(); + if (!$scope.$parent.isSidebarVisible()) { + $scope.$parent.showSidebar(); + } + setTimeout(function() { + document.getElementById('bufferFilter').focus(); + }); + return true; + } + + var caretPos; + + // Arrow up -> go up in history + if ($event.type === "keydown" && code === 38 && document.activeElement === inputNode) { + caretPos = inputNode.selectionStart; + if ($scope.command.slice(0, caretPos).indexOf("\n") !== -1) { + return false; + } + $scope.command = models.getActiveBuffer().getHistoryUp($scope.command); + // Set cursor to last position. Need 0ms timeout because browser sets cursor + // position to the beginning after this key handler returns. + setTimeout(function() { + if ($scope.command) { + inputNode.setSelectionRange($scope.command.length, $scope.command.length); + } + }, 0); + return true; + } + + // Arrow down -> go down in history + if ($event.type === "keydown" && code === 40 && document.activeElement === inputNode) { + caretPos = inputNode.selectionStart; + if ($scope.command.slice(caretPos).indexOf("\n") !== -1) { + return false; + } + $scope.command = models.getActiveBuffer().getHistoryDown($scope.command); + // We don't need to set the cursor to the rightmost position here, the browser does that for us + return true; + } + + // Enter to submit, shift-enter for newline + if (code == 13 && !$event.shiftKey && document.activeElement === inputNode) { + $event.preventDefault(); + $scope.sendMessage(); + return true; + } + + var bufferlines = document.getElementById("bufferlines"); + var lines; + var i; + + // Page up -> scroll up + if ($event.type === "keydown" && code === 33 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) { + if (bufferlines.scrollTop === 0) { + if (!$rootScope.loadingLines) { + $scope.$parent.fetchMoreLines(); + } + return true; + } + lines = bufferlines.querySelectorAll("tr"); + for (i = lines.length - 1; i >= 0; i--) { + if ((lines[i].offsetTop-bufferlines.scrollTop) scroll down + if ($event.type === "keydown" && code === 34 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) { + lines = bufferlines.querySelectorAll("tr"); + for (i = 0; i < lines.length; i++) { + if ((lines[i].offsetTop-bufferlines.scrollTop)>bufferlines.clientHeight/2) { + lines[i].scrollIntoView(true); + break; + } + } + return true; + } + + // Some readline keybindings + if (settings.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) { + // get current caret position + caretPos = inputNode.selectionStart; + // Ctrl-a + if (code == 65) { + inputNode.setSelectionRange(0, 0); + // Ctrl-e + } else if (code == 69) { + inputNode.setSelectionRange($scope.command.length, $scope.command.length); + // Ctrl-u + } else if (code == 85) { + $scope.command = $scope.command.slice(caretPos); + setTimeout(function() { + inputNode.setSelectionRange(0, 0); + }); + // Ctrl-k + } else if (code == 75) { + $scope.command = $scope.command.slice(0, caretPos); + setTimeout(function() { + inputNode.setSelectionRange($scope.command.length, $scope.command.length); + }); + // Ctrl-w + } else if (code == 87) { + var trimmedValue = $scope.command.slice(0, caretPos); + var lastSpace = trimmedValue.lastIndexOf(' ') + 1; + $scope.command = $scope.command.slice(0, lastSpace) + $scope.command.slice(caretPos, $scope.command.length); + setTimeout(function() { + inputNode.setSelectionRange(lastSpace, lastSpace); + }); + } else { + return false; + } + $event.preventDefault(); + return true; + } + + // Alt key down -> display quick key legend + if ($event.type === "keydown" && code === 18 && !$event.ctrlKey && !$event.shiftKey) { + $rootScope.showQuickKeys = true; + } + }; + + $rootScope.handleKeyRelease = function($event) { + // Alt key up -> remove quick key legend + if ($event.keyCode === 18) { + if ($rootScope.quickKeysTimer !== undefined) { + clearTimeout($rootScope.quickKeysTimer); + } + $rootScope.quickKeysTimer = setTimeout(function() { + if ($rootScope.showQuickKeys) { + $rootScope.showQuickKeys = false; + $rootScope.$apply(); + } + delete $rootScope.quickKeysTimer; + }, 1000); + return true; + } + }; + }] + }; +}); +})(); diff --git a/sources/js/irc-utils.js b/sources/js/irc-utils.js new file mode 100644 index 0000000..a6deef0 --- /dev/null +++ b/sources/js/irc-utils.js @@ -0,0 +1,228 @@ +/** + * Portable utilities for IRC. + */ + +(function() { +'use strict'; + +var IrcUtils = angular.module('IrcUtils', []); + +IrcUtils.service('IrcUtils', [function() { + /** + * Escape a string for usage in a larger regexp + * @param str String to escape + * @return Escaped string + */ + var escapeRegExp = function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }; + + /** + * Get a new version of a nick list, sorted by last speaker + * + * @param nickList Original nick list + * @return Sorted nick list + */ + var _ciNickList = function(nickList) { + + var newList = _(nickList).sortBy(function(nickObj) { + return -nickObj.spokeAt; + }); + newList = _(newList).pluck('name'); + + return newList; + }; + + /** + * Completes a single nick. + * + * @param candidate What to search for + * @param nickList Array of current nicks sorted for case insensitive searching + * @return Completed nick (null if not found) + */ + var _completeSingleNick = function(candidate, nickList) { + var foundNick = null; + + nickList.some(function(nick) { + if (nick.toLowerCase().search(candidate.toLowerCase()) === 0) { + // found! + foundNick = nick; + return true; + } + return false; + }); + + return foundNick; + }; + + /** + * Get the next nick when iterating nicks. + * + * @param iterCandidate First characters to look at + * @param currentNick Current selected nick + * @param nickList Array of current nicks sorted for case insensitive searching + * @return Next nick (may be the same) + */ + var _nextNick = function(iterCandidate, currentNick, nickList) { + var matchingNicks = []; + var at = null; + var lcIterCandidate = iterCandidate.toLowerCase(); + var lcCurrentNick = currentNick.toLowerCase(); + + // collect matching nicks + for (var i = 0; i < nickList.length; ++i) { + var lcNick = nickList[i].toLowerCase(); + if (lcNick.search(escapeRegExp(lcIterCandidate)) === 0) { + matchingNicks.push(nickList[i]); + if (lcCurrentNick === lcNick) { + at = matchingNicks.length - 1; + } + } + /* Since we aren't sorted any more torhve disabled this: + else if (matchingNicks.length > 0) { + // end of group, no need to check after this + //break; + } + */ + } + + if (at === null || matchingNicks.length === 0) { + return currentNick; + } else { + ++at; + if (at === matchingNicks.length) { + // cycle + at = 0; + } + return matchingNicks[at]; + } + }; + + /** + * Nicks tab completion. + * + * @param text Plain text (no colors) + * @param caretPos Current caret position (0 means before the first character) + * @param iterCandidate Current iteration candidate (null if not iterating) + * @param nickList Array of current nicks + * @param suf Custom suffix (at least one character, escaped for regex) + * @return Object with following properties: + * text: new complete replacement text + * caretPos: new caret position within new text + * foundNick: completed nick (or null if not possible) + * iterCandidate: current iterating candidate + */ + var completeNick = function(text, caretPos, iterCandidate, nickList, suf) { + var doIterate = (iterCandidate !== null); + if (suf === null) { + suf = ':'; + } + + // new nick list to search in + var searchNickList = _ciNickList(nickList); + + // text before and after caret + var beforeCaret = text.substring(0, caretPos); + var afterCaret = text.substring(caretPos); + + // default: don't change anything + var ret = { + text: text, + caretPos: caretPos, + foundNick: null, + iterCandidate: null + }; + + // iterating nicks at the beginning? + var m = beforeCaret.match(new RegExp('^([a-zA-Z0-9_\\\\\\[\\]{}^`|-]+)' + suf + ' $')); + + var newNick = null; + if (m) { + if (doIterate) { + // try iterating + newNick = _nextNick(iterCandidate, m[1], searchNickList); + beforeCaret = newNick + suf + ' '; + return { + text: beforeCaret + afterCaret, + caretPos: beforeCaret.length, + foundNick: newNick, + iterCandidate: iterCandidate + }; + } else { + // if not iterating, don't do anything + return ret; + } + } + + // nick completion in the beginning? + m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/); + if (m) { + // try completing + newNick = _completeSingleNick(escapeRegExp(m[1]), searchNickList); + if (newNick === null) { + // no match + return ret; + } + beforeCaret = newNick + suf + ' '; + if (afterCaret[0] === ' ') { + // swallow first space after caret if any + afterCaret = afterCaret.substring(1); + } + return { + text: beforeCaret + afterCaret, + caretPos: beforeCaret.length, + foundNick: newNick, + iterCandidate: m[1] + }; + } + + // iterating nicks in the middle? + m = beforeCaret.match(/^(.* )([a-zA-Z0-9_\\\[\]{}^`|-]+) $/); + if (m) { + if (doIterate) { + // try iterating + newNick = _nextNick(iterCandidate, m[2], searchNickList); + beforeCaret = m[1] + newNick + ' '; + return { + text: beforeCaret + afterCaret, + caretPos: beforeCaret.length, + foundNick: newNick, + iterCandidate: iterCandidate + }; + } else { + // if not iterating, don't do anything + return ret; + } + } + + // nick completion elsewhere in the middle? + m = beforeCaret.match(/^(.* )([a-zA-Z0-9_\\\[\]{}^`|-]+)$/); + if (m) { + // try completing + newNick = _completeSingleNick(m[2], searchNickList); + if (newNick === null) { + // no match + return ret; + } + beforeCaret = m[1] + newNick + ' '; + if (afterCaret[0] === ' ') { + // swallow first space after caret if any + afterCaret = afterCaret.substring(1); + } + return { + text: beforeCaret + afterCaret, + caretPos: beforeCaret.length, + foundNick: newNick, + iterCandidate: m[2] + }; + } + + // completion not possible + return ret; + }; + + return { + 'completeNick': completeNick + }; +}]); +})(); diff --git a/sources/js/localstorage.js b/sources/js/localstorage.js new file mode 100644 index 0000000..0c1218e --- /dev/null +++ b/sources/js/localstorage.js @@ -0,0 +1,117 @@ +(function() { +'use strict'; + +var ls = angular.module('localStorage',[]); + +ls.factory("$store", ["$parse", function($parse){ + /** + * Global Vars + */ + var storage = (typeof window.localStorage === 'undefined') ? undefined : window.localStorage, + supported = !(typeof storage == 'undefined' || typeof window.JSON == 'undefined'); + + if (!supported) { + console.log('Warning: localStorage is not supported'); + } + + var privateMethods = { + /** + * Pass any type of a string from the localStorage to be parsed so it returns a usable version (like an Object) + * @param res - a string that will be parsed for type + * @returns {*} - whatever the real type of stored value was + */ + parseValue: function(res) { + var val; + try { + val = JSON.parse(res); + if (val === undefined){ + val = res; + } + if (val === 'true'){ + val = true; + } + if (val === 'false'){ + val = false; + } + if (parseFloat(val) == val && !angular.isObject(val)) { + val = parseFloat(val); + } + } catch(e){ + val = res; + } + return val; + } + }; + var publicMethods = { + /** + * Set - lets you set a new localStorage key pair set + * @param key - a string that will be used as the accessor for the pair + * @param value - the value of the localStorage item + * @returns {*} - will return whatever it is you've stored in the local storage + */ + set: function(key,value){ + if (!supported){ + console.log('Local Storage not supported'); + } + var saver = JSON.stringify(value); + storage.setItem(key, saver); + return privateMethods.parseValue(saver); + }, + /** + * Get - lets you get the value of any pair you've stored + * @param key - the string that you set as accessor for the pair + * @returns {*} - Object,String,Float,Boolean depending on what you stored + */ + get: function(key){ + if (!supported){ + return null; + } + var item = storage.getItem(key); + return privateMethods.parseValue(item); + }, + /** + * Remove - lets you nuke a value from localStorage + * @param key - the accessor value + * @returns {boolean} - if everything went as planned + */ + remove: function(key) { + if (!supported){ + return false; + } + storage.removeItem(key); + return true; + }, + /** + * Enumerate all keys + */ + enumerateKeys: function() { + var keys = []; + for (var i = 0, len = storage.length; i < len; ++i) { + keys.push(storage.key(i)); + } + return keys; + }, + /** + * Bind - lets you directly bind a localStorage value to a $scope variable + * @param $scope - the current scope you want the variable available in + * @param key - the name of the variable you are binding + * @param def - the default value (OPTIONAL) + * @returns {*} - returns whatever the stored value is + */ + bind: function ($scope, key, def) { + if (def === undefined) { + def = ''; + } + if (publicMethods.get(key) === undefined || publicMethods.get(key) === null) { + publicMethods.set(key, def); + } + $parse(key).assign($scope, publicMethods.get(key)); + $scope.$watch(key, function (val) { + publicMethods.set(key, val); + }, true); + return publicMethods.get(key); + } + }; + return publicMethods; +}]); +})(); diff --git a/sources/js/models.js b/sources/js/models.js new file mode 100644 index 0000000..f6d7f63 --- /dev/null +++ b/sources/js/models.js @@ -0,0 +1,596 @@ +/* + * This file contains the weechat models and various + * helper methods to work with them. + */ +(function() { +'use strict'; + +var models = angular.module('weechatModels', []); + +models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) { + // WeeChat version + this.version = null; + + // WeeChat configuration values + this.wconfig = {}; + + // Save outgoing queries + this.outgoingQueries = []; + + var parseRichText = function(text) { + var textElements = weeChat.Protocol.rawText2Rich(text), + typeToClassPrefixFg = { + 'option': 'cof-', + 'weechat': 'cwf-', + 'ext': 'cef-' + }, + typeToClassPrefixBg = { + 'option': 'cob-', + 'weechat': 'cwb-', + 'ext': 'ceb-' + }; + + textElements.forEach(function(textEl) { + textEl.classes = []; + + // foreground color + var prefix = typeToClassPrefixFg[textEl.fgColor.type]; + textEl.classes.push(prefix + textEl.fgColor.name); + + // background color + prefix = typeToClassPrefixBg[textEl.bgColor.type]; + textEl.classes.push(prefix + textEl.bgColor.name); + + // attributes + if (textEl.attrs.name !== null) { + textEl.classes.push('coa-' + textEl.attrs.name); + } + var attr, val; + for (attr in textEl.attrs.override) { + val = textEl.attrs.override[attr]; + if (val) { + textEl.classes.push('a-' + attr); + } else { + textEl.classes.push('a-no-' + attr); + } + } + }); + return textElements; + }; + this.parseRichText = parseRichText; + + /* + * Buffer class + */ + this.Buffer = function(message) { + // weechat properties + var fullName = message.full_name; + var shortName = message.short_name; + var hidden = message.hidden; + // If it's a channel, trim away the prefix (#, &, or +). If that is empty and the buffer + // has a short name, use a space (because the prefix will be displayed separately, and we don't want + // prefix + fullname, which would happen otherwise). Else, use null so that full_name is used + var trimmedName = shortName.replace(/^[#&+]/, '') || (shortName ? ' ' : null); + // get channel identifier + var prefix = ['#', '&', '+'].indexOf(shortName.charAt(0)) >= 0 ? shortName.charAt(0) : ''; + var title = parseRichText(message.title); + var number = message.number; + var pointer = message.pointers[0]; + var notify = 3; // Default 3 == message + var lines = []; + var requestedLines = 0; + var allLinesFetched = false; + var nicklist = {}; + var history = []; + var historyPos = 0; + var active = false; + var notification = 0; + var unread = 0; + var lastSeen = -1; + // There are two kinds of types: bufferType (free vs formatted) and + // the kind of type that distinguishes queries from channels etc + var bufferType = message.type; + var type = message.local_variables.type; + var indent = (['channel', 'private'].indexOf(type) >= 0); + + var plugin = message.local_variables.plugin; + var server = message.local_variables.server; + // Server buffers have this "irc.server.freenode" naming schema, which + // messes the sorting up. We need it to be "irc.freenode" instead. + var serverSortKey = plugin + "." + server + + (type === "server" ? "" : ("." + shortName)); + // Lowercase it so alt+up/down traverses buffers in the same order + // angular's sortBy directive puts them in + serverSortKey = serverSortKey.toLowerCase(); + + // Buffer opened message does not include notify level + if (message.notify !== undefined) { + notify = message.notify; + } + + var rtitle = ""; + for (var i = 0; i < title.length; ++i) { + rtitle += title[i].text; + } + + /* + * Adds a line to this buffer + * + * @param line the BufferLine object + * @return undefined + */ + var addLine = function(line) { + lines.push(line); + updateNickSpeak(line); + }; + + /* + * Adds a nick to nicklist + */ + var addNick = function(group, nick) { + if (nicklistRequested()) { + nick.spokeAt = Date.now(); + nicklist[group].nicks.push(nick); + } + }; + /* + * Deletes a nick from nicklist + */ + var delNick = function(group, nick) { + group = nicklist[group]; + if (group === undefined) { + return; + } + group.nicks = _.filter(group.nicks, function(n) { return n.name !== nick.name;}); + /* + for (i in group.nicks) { + if (group.nicks[i].name == nick.name) { + delete group.nicks[i]; + break; + } + } + */ + }; + /* + * Updates a nick in nicklist + */ + var updateNick = function(group, nick) { + group = nicklist[group]; + if (group === undefined) { + // We are getting nicklist events for a buffer where not yet + // have populated the nicklist, so there will be nothing to + // update. Just ignore the event. + return; + } + for(var i in group.nicks) { + if (group.nicks[i].name === nick.name) { + group.nicks[i] = nick; + break; + } + } + }; + + /* + * Update a nick with a fresh timestamp so tab completion + * can use time to complete recent speakers + */ + var updateNickSpeak = function(line) { + // Try to find nick from prefix + var prefix = line.prefix; + if (prefix.length === 0) { + // some scripts produce lines without a prefix + return; + } + var nick = prefix[prefix.length - 1].text; + // Action / me, find the nick as the first word of the message + if (nick === " *") { + var match = line.text.match(/^(.+)\s/); + if (match) { + nick = match[1]; + } + } + else if (nick === "" || nick === "=!=") { + return; + } + _.each(nicklist, function(nickGroup) { + _.each(nickGroup.nicks, function(nickObj) { + if (nickObj.name === nick) { + // Use the order the line arrive in for simplicity + // instead of using weechat's own timestamp + nickObj.spokeAt = Date.now(); + } + }); + }); + }; + + /* + * Get a flat nicklist sorted by speaker time. This function is + * called for every tab key press by the user. + * + */ + var getNicklistByTime = function() { + var newlist = []; + _.each(nicklist, function(nickGroup) { + _.each(nickGroup.nicks, function(nickObj) { + newlist.push(nickObj); + }); + }); + + newlist.sort(function(a, b) { + return a.spokeAt < b.spokeAt; + }); + + return newlist; + }; + + var addToHistory = function(line) { + var result = ""; + if (historyPos !== history.length) { + // Pop cached line from history. Occurs if we submit something from history + result = history.pop(); + } + history.push(line); + historyPos = history.length; // Go to end of history + return result; + }; + + var getHistoryUp = function(currentLine) { + if (historyPos >= history.length) { + // cache current line in history + history.push(currentLine); + } + if (historyPos <= 0 || historyPos >= history.length) { + // Can't go up from first message or from out-of-bounds index + return currentLine; + } else { + // Go up in history + historyPos--; + var line = history[historyPos]; + return line; + } + }; + + var getHistoryDown = function(currentLine) { + if (historyPos === history.length) { + // stash on history like weechat does + if (currentLine !== undefined && currentLine !== '') { + history.push(currentLine); + historyPos++; + } + return ''; + } else if (historyPos < 0 || historyPos > history.length) { + // Can't go down from out of bounds or last message + return currentLine; + } else { + historyPos++; + + if (history.length > 0 && historyPos == (history.length-1)) { + // return cached line and remove from cache + return history.pop(); + } else { + // Go down in history + return history[historyPos]; + } + } + }; + + + // Check if the nicklist is empty, i.e., no nicks present + // This checks for the presence of people, not whether a + // request for the nicklist has been made + var isNicklistEmpty = function() { + for (var obj in nicklist) { + if (obj !== 'root') { + return false; + } + } + return true; + }; + + var nicklistRequested = function() { + // If the nicklist has been requested but is empty, it + // still has a 'root' property. Check for its existence. + return nicklist.hasOwnProperty('root'); + }; + + /* Clear all our buffer lines */ + var clear = function() { + while(lines.length > 0) { + lines.pop(); + } + requestedLines = 0; + }; + + return { + id: pointer, + fullName: fullName, + shortName: shortName, + hidden: hidden, + trimmedName: trimmedName, + prefix: prefix, + number: number, + title: title, + rtitle: rtitle, + lines: lines, + clear: clear, + requestedLines: requestedLines, + addLine: addLine, + lastSeen: lastSeen, + unread: unread, + notification: notification, + notify: notify, + nicklist: nicklist, + addNick: addNick, + delNick: delNick, + updateNick: updateNick, + getNicklistByTime: getNicklistByTime, + serverSortKey: serverSortKey, + indent: indent, + bufferType: bufferType, + type: type, + plugin: plugin, + server: server, + history: history, + addToHistory: addToHistory, + getHistoryUp: getHistoryUp, + getHistoryDown: getHistoryDown, + isNicklistEmpty: isNicklistEmpty, + nicklistRequested: nicklistRequested + }; + + }; + + /* + * BufferLine class + */ + this.BufferLine = function(message) { + var buffer = message.buffer; + var date = message.date; + var shortTime = $filter('date')(date, 'HH:mm'); + + var prefix = parseRichText(message.prefix); + var tags_array = message.tags_array; + var displayed = message.displayed; + var highlight = message.highlight; + var content = parseRichText(message.message); + + if (highlight) { + prefix.forEach(function(textEl) { + textEl.classes.push('highlight'); + }); + } + + var rtext = ""; + for (var i = 0; i < content.length; ++i) { + rtext += content[i].text; + } + + return { + prefix: prefix, + content: content, + date: date, + shortTime: shortTime, + buffer: buffer, + tags: tags_array, + highlight: highlight, + displayed: displayed, + text: rtext + + }; + + }; + + function nickGetColorClasses(nickMsg, propName) { + if (propName in nickMsg && nickMsg[propName] && nickMsg[propName].length > 0) { + var color = nickMsg[propName]; + if (color.match(/^weechat/)) { + // color option + var colorName = color.match(/[a-zA-Z0-9_]+$/)[0]; + return [ + 'cof-' + colorName, + 'cob-' + colorName, + 'coa-' + colorName + ]; + } else if (color.match(/^[a-zA-Z]+$/)) { + // WeeChat color name + return [ + 'cwf-' + color + ]; + } else if (color.match(/^[0-9]+$/)) { + // extended color + return [ + 'cef-' + color + ]; + } + + } + + return [ + 'cwf-default' + ]; + } + + function nickGetClasses(nickMsg) { + return { + 'name': nickGetColorClasses(nickMsg, 'color'), + 'prefix': nickGetColorClasses(nickMsg, 'prefix_color') + }; + } + + /* + * Nick class + */ + this.Nick = function(message) { + var prefix = message.prefix; + var visible = message.visible; + var name = message.name; + var colorClasses = nickGetClasses(message); + + return { + prefix: prefix, + visible: visible, + name: name, + prefixClasses: colorClasses.prefix, + nameClasses: colorClasses.name + }; + }; + /* + * Nicklist Group class + */ + this.NickGroup = function(message) { + var name = message.name; + var visible = message.visible; + var nicks = []; + + return { + name: name, + visible: visible, + nicks: nicks + }; + }; + + + var activeBuffer = null; + var previousBuffer = null; + + this.model = { 'buffers': {} }; + + /* + * Adds a buffer to the list + * + * @param buffer buffer object + * @return undefined + */ + this.addBuffer = function(buffer) { + this.model.buffers[buffer.id] = buffer; + }; + + /* + * Returns the current active buffer + * + * @return active buffer object + */ + this.getActiveBuffer = function() { + return activeBuffer; + }; + + /* + * Returns a reference to the currently active buffer that + * WeeChat understands without crashing, even if it's invalid + * + * @return active buffer pointer (WeeChat 1.0+) or fullname (older versions) + */ + this.getActiveBufferReference = function() { + if (this.version !== null && this.version[0] >= 1) { + // pointers are being validated, they're more reliable than + // fullName (e.g. if fullName contains spaces) + return "0x"+activeBuffer.id; + } else { + return activeBuffer.fullName; + } + }; + + /* + * Returns the previous current active buffer + * + * @return previous buffer object + */ + this.getPreviousBuffer = function() { + return previousBuffer; + }; + + /* + * Sets the buffer specifiee by bufferId as active. + * Deactivates the previous current buffer. + * + * @param bufferId id of the new active buffer + * @return true on success, false if buffer was not found + */ + this.setActiveBuffer = function(bufferId, key) { + if (key === undefined) { + key = 'id'; + } + + previousBuffer = this.getActiveBuffer(); + + if (key === 'id') { + activeBuffer = this.model.buffers[bufferId]; + } + else { + activeBuffer = _.find(this.model.buffers, function(buffer) { + if (buffer[key] === bufferId) { + return buffer; + } + }); + } + + if (activeBuffer === undefined) { + // Buffer not found, undo assignment + activeBuffer = previousBuffer; + return false; + } + + if (previousBuffer) { + // turn off the active status for the previous buffer + previousBuffer.active = false; + // Save the last line we saw + previousBuffer.lastSeen = previousBuffer.lines.length-1; + } + + var unreadSum = activeBuffer.unread + activeBuffer.notification; + + activeBuffer.active = true; + activeBuffer.unread = 0; + activeBuffer.notification = 0; + + $rootScope.$emit('activeBufferChanged', unreadSum); + $rootScope.$emit('notificationChanged'); + return true; + }; + + /* + * Returns the buffer list + */ + this.getBuffers = function() { + return this.model.buffers; + }; + + /* + * Reinitializes the model + */ + this.reinitialize = function() { + this.model.buffers = {}; + }; + + /* + * Returns a specific buffer object + * + * @param bufferId id of the buffer + * @return the buffer object + */ + this.getBuffer = function(bufferId) { + return this.model.buffers[bufferId]; + }; + + /* + * Closes a weechat buffer. Sets the first buffer + * as active, if the closing buffer was active before + * + * @param bufferId id of the buffer to close + * @return undefined + */ + this.closeBuffer = function(bufferId) { + var buffer = this.getBuffer(bufferId); + // Check if the buffer really exists, just in case + if (buffer === undefined) { + return; + } + if (buffer.active) { + var firstBuffer = _.keys(this.model.buffers)[0]; + this.setActiveBuffer(firstBuffer); + } + // Can't use `buffer` here, needs to be deleted from the list + delete(this.model.buffers[bufferId]); + }; +}]); +})(); diff --git a/sources/js/notifications.js b/sources/js/notifications.js new file mode 100644 index 0000000..0f79a15 --- /dev/null +++ b/sources/js/notifications.js @@ -0,0 +1,210 @@ +var weechat = angular.module('weechat'); + +weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', function($rootScope, $log, models, settings) { + var serviceworker = false; + var notifications = []; + // Ask for permission to display desktop notifications + var requestNotificationPermission = function() { + // Firefox + if (window.Notification) { + Notification.requestPermission(function(status) { + $log.info('Notification permission status: ', status); + if (Notification.permission !== status) { + Notification.permission = status; + } + }); + } + + // Webkit + if (window.webkitNotifications !== undefined) { + var havePermission = window.webkitNotifications.checkPermission(); + if (havePermission !== 0) { // 0 is PERMISSION_ALLOWED + $log.info('Notification permission status: ', havePermission === 0); + window.webkitNotifications.requestPermission(); + } + } + + if ('serviceWorker' in navigator) { + $log.info('Service Worker is supported'); + navigator.serviceWorker.register('serviceworker.js').then(function(reg) { + $log.info('Service Worker install:', reg); + serviceworker = true; + }).catch(function(err) { + $log.info('Service Worker err:', err); + }); + } + }; + + var showNotification = function(buffer, title, body) { + if (serviceworker) { + navigator.serviceWorker.ready.then(function(registration) { + registration.showNotification(title, { + body: body, + icon: 'assets/img/glowing_bear_128x128.png', + vibrate: [200, 100], + tag: 'gb-highlight-vib' + }); + }); + } else if (typeof Windows !== 'undefined' && typeof Windows.UI !== 'undefined' && typeof Windows.UI.Notifications !== 'undefined') { + + var winNotifications = Windows.UI.Notifications; + var toastNotifier = winNotifications.ToastNotificationManager.createToastNotifier(); + var template = winNotifications.ToastTemplateType.toastText02; + var toastXml = winNotifications.ToastNotificationManager.getTemplateContent(template); + var toastTextElements = toastXml.getElementsByTagName("text"); + + toastTextElements[0].appendChild(toastXml.createTextNode(title)); + toastTextElements[1].appendChild(toastXml.createTextNode(body)); + + var toast = new winNotifications.ToastNotification(toastXml); + + toast.onactivated = function() { + models.setActiveBuffer(buffer.id); + window.focus(); + }; + + toastNotifier.show(toast); + + } else { + + var notification = new Notification(title, { + body: body, + icon: 'assets/img/favicon.png' + }); + + // Save notification, so we can close all outstanding ones when disconnecting + notification.id = notifications.length; + notifications.push(notification); + + // Cancel notification automatically + var timeout = 15*1000; + notification.onshow = function() { + setTimeout(function() { + notification.close(); + }, timeout); + }; + + // Click takes the user to the buffer + notification.onclick = function() { + models.setActiveBuffer(buffer.id); + window.focus(); + notification.close(); + }; + + // Remove from list of active notifications + notification.onclose = function() { + delete notifications[this.id]; + }; + + } + + }; + + + // Reduce buffers with "+" operation over a key. Mostly useful for unread/notification counts. + var unreadCount = function(type) { + if (!type) { + type = "unread"; + } + + // Do this the old-fashioned way with iterating over the keys, as underscore proved to be error-prone + var keys = Object.keys(models.model.buffers); + var count = 0; + for (var key in keys) { + count += models.model.buffers[keys[key]][type]; + } + + return count; + }; + + + var updateTitle = function() { + var notifications = unreadCount('notification'); + if (notifications > 0) { + // New notifications deserve an exclamation mark + $rootScope.notificationStatus = '(' + notifications + ') '; + } else { + $rootScope.notificationStatus = ''; + } + + var activeBuffer = models.getActiveBuffer(); + if (activeBuffer) { + $rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.rtitle; + } + }; + + var updateFavico = function() { + var notifications = unreadCount('notification'); + if (notifications > 0) { + $rootScope.favico.badge(notifications, { + bgColor: '#d00', + textColor: '#fff' + }); + } else { + var unread = unreadCount('unread'); + if (unread === 0) { + $rootScope.favico.reset(); + } else { + $rootScope.favico.badge(unread, { + bgColor: '#5CB85C', + textColor: '#ff0' + }); + } + } + }; + + /* Function gets called from bufferLineAdded code if user should be notified */ + var createHighlight = function(buffer, message) { + var title = ''; + var body = ''; + var numNotifications = buffer.notification; + + if (buffer.type === "private") { + if (numNotifications > 1) { + title = numNotifications.toString() + ' private messages from '; + } else { + title = 'Private message from '; + } + body = message.text; + } else { + if (numNotifications > 1) { + title = numNotifications.toString() + ' highlights in '; + } else { + title = 'Highlight in '; + } + var prefix = ''; + for (var i = 0; i < message.prefix.length; i++) { + prefix += message.prefix[i].text; + } + body = '<' + prefix + '> ' + message.text; + } + title += buffer.shortName + " (" + buffer.server + ")"; + + showNotification(buffer, title, body); + + if (settings.soundnotification) { + // TODO fill in a sound file + var audioFile = "assets/audio/sonar"; + var soundHTML = ''; + document.getElementById("soundNotification").innerHTML = soundHTML; + } + }; + + var cancelAll = function() { + while (notifications.length > 0) { + var notification = notifications.pop(); + if (notification !== undefined) { + notification.close(); + } + } + }; + + return { + requestNotificationPermission: requestNotificationPermission, + updateTitle: updateTitle, + updateFavico: updateFavico, + createHighlight: createHighlight, + cancelAll: cancelAll, + unreadCount: unreadCount + }; +}]); diff --git a/sources/js/plugin-directive.js b/sources/js/plugin-directive.js new file mode 100644 index 0000000..4510f04 --- /dev/null +++ b/sources/js/plugin-directive.js @@ -0,0 +1,86 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat'); + +weechat.directive('plugin', ['$rootScope', 'settings', function($rootScope, settings) { + /* + * Plugin directive + * Shows additional plugin content + */ + return { + templateUrl: 'directives/plugin.html', + + scope: { + plugin: '=data' + }, + + controller: ['$scope', function($scope) { + + $scope.displayedContent = ""; + + // Auto-display embedded content only if it isn't NSFW + $scope.plugin.visible = !settings.noembed && !$scope.plugin.nsfw; + + // user-accessible hash key that is a valid CSS class name + $scope.plugin.className = "embed_" + $scope.plugin.$$hashKey.replace(':','_'); + + $scope.plugin.getElement = function() { + return document.querySelector("." + $scope.plugin.className); + }; + + $scope.hideContent = function() { + $scope.plugin.visible = false; + }; + + $scope.showContent = function(automated) { + /* + * Shows the plugin content. + * displayedContent is bound to the DOM. + * Actual plugin content is only fetched when + * content is shown. + */ + + var embed = $scope.plugin.getElement(); + + // If the plugin is asynchronous / lazy, execute it now and let it insert itself + // TODO store the result between channel switches + if ($scope.plugin.content instanceof Function){ + // Don't rerun if the result is already there + if (!embed || embed.innerHTML === "") { + // if we're autoshowing, the element doesn't exist yet, and we need + // to do this async (wrapped in a setTimeout) + setTimeout(function() { + $scope.plugin.content(); + }); + } + } else { + $scope.displayedContent = $scope.plugin.content; + } + $scope.plugin.visible = true; + + // Scroll embed content into view + var scroll; + if (automated) { + var wasBottom = $rootScope.bufferBottom; + scroll = function() { + $rootScope.updateBufferBottom(wasBottom); + }; + } else { + scroll = function() { + if (embed && embed.scrollIntoViewIfNeeded !== undefined) { + embed.scrollIntoViewIfNeeded(); + $rootScope.updateBufferBottom(); + } + }; + } + setTimeout(scroll, 500); + }; + + if ($scope.plugin.visible) { + $scope.showContent(true); + } + }] + }; +}]); +})(); diff --git a/sources/js/plugins.js b/sources/js/plugins.js new file mode 100644 index 0000000..7ba506d --- /dev/null +++ b/sources/js/plugins.js @@ -0,0 +1,530 @@ +/* + * This file contains the plugin definitions + */ + +(function() { +'use strict'; + +var plugins = angular.module('plugins', []); + +/* + * Definition of a user provided plugin with sensible default values + * + * User plugins are created by providing a name and a contentForMessage + * function that parses a string and returns any additional content. + */ +var Plugin = function(name, contentForMessage) { + return { + contentForMessage: contentForMessage, + exclusive: false, + name: name + }; +}; + + +// Regular expression that detects URLs for UrlPlugin +var urlRegexp = /(?:(?:https?|ftp):\/\/|www\.|ftp\.)\S*[^\s.;,(){}<>]/g; +/* + * Definition of a user provided plugin that consumes URLs + * + * URL plugins are created by providing a name and a function that + * that parses a URL and returns any additional content. + */ +var UrlPlugin = function(name, urlCallback) { + return { + contentForMessage: function(message) { + var urls = message.match(urlRegexp); + var content = []; + + for (var i = 0; urls && i < urls.length; i++) { + var result = urlCallback(urls[i]); + if (result) { + content.push(result); + } + } + return content; + }, + exclusive: false, + name: name + }; +}; + +/* + * This service provides access to the plugin manager + * + * The plugin manager is where the various user provided plugins + * are registered. It is responsible for finding additional content + * to display when messages are received. + * + */ + plugins.service('plugins', ['userPlugins', '$sce', function(userPlugins, $sce) { + + /* + * Defines the plugin manager object + */ + var PluginManagerObject = function() { + + var plugins = []; + + /* + * Register the user provides plugins + * + * @param userPlugins user provided plugins + */ + var registerPlugins = function(userPlugins) { + for (var i = 0; i < userPlugins.length; i++) { + plugins.push(userPlugins[i]); + } + }; + + var nsfwRegexp = new RegExp('nsfw', 'i'); + + /* + * Iterates through all the registered plugins + * and run their contentForMessage function. + */ + var contentForMessage = function(message) { + message.metadata = []; + var addPluginContent = function(content, pluginName, num) { + if (num) { + pluginName += " " + num; + } + + // If content isn't a callback, it's HTML + if (!(content instanceof Function)) { + content = $sce.trustAsHtml(content); + } + + message.metadata.push({ + 'content': content, + 'nsfw': nsfw, + 'name': pluginName + }); + }; + + for (var i = 0; i < plugins.length; i++) { + + var nsfw = false; + if (message.text.match(nsfwRegexp)) { + nsfw = true; + } + + var pluginContent = plugins[i].contentForMessage( + message.text + ); + if (pluginContent && pluginContent !== []) { + + if (pluginContent instanceof Array) { + for (var j = pluginContent.length - 1; j >= 0; j--) { + // only give a number if there are multiple embeds + var num = (pluginContent.length == 1) ? undefined : (j + 1); + addPluginContent(pluginContent[j], plugins[i].name, num); + } + } else { + addPluginContent(pluginContent, plugins[i].name); + } + + if (plugins[i].exclusive) { + break; + } + } + } + + return message; + }; + + return { + registerPlugins: registerPlugins, + contentForMessage: contentForMessage + }; + }; + + // Instanciates and registers the plugin manager. + this.PluginManager = new PluginManagerObject(); + this.PluginManager.registerPlugins(userPlugins.plugins); + +}]); + +/* + * This factory exposes the collection of user provided plugins. + * + * To create your own plugin, you need to: + * + * 1. Define its contentForMessage function. The contentForMessage + * function takes a string as a parameter and returns a HTML string. + * + * 2. Instantiate a Plugin object with contentForMessage function as its + * argument. + * + * 3. Add it to the plugins array. + * + */ +plugins.factory('userPlugins', function() { + // standard JSONp origin policy trick + var jsonp = function (url, callback) { + var callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random()); + window[callbackName] = function(data) { + delete window[callbackName]; + document.body.removeChild(script); + callback(data); + }; + + var script = document.createElement('script'); + script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'callback=' + callbackName; + document.body.appendChild(script); + }; + + /* + * Spotify Embedded Player + * + * See: https://developer.spotify.com/technologies/widgets/spotify-play-button/ + * + */ + + var spotifyPlugin = new Plugin('Spotify track', function(message) { + var content = []; + var addMatch = function(match) { + for (var i = 0; match && i < match.length; i++) { + var id = match[i].substr(match[i].length - 22, match[i].length); + var element = angular.element('') + .attr('src', '//embed.spotify.com/?uri=spotify:track:' + id) + .attr('width', '300') + .attr('height', '80') + .attr('frameborder', '0') + .attr('allowtransparency', 'true'); + content.push(element.prop('outerHTML')); + } + }; + addMatch(message.match(/spotify:track:([a-zA-Z-0-9]{22})/g)); + addMatch(message.match(/open.spotify.com\/track\/([a-zA-Z-0-9]{22})/g)); + return content; + }); + + /* + * YouTube Embedded Player + * + * See: https://developers.google.com/youtube/player_parameters + */ + var youtubePlugin = new UrlPlugin('YouTube video', function(url) { + var regex = /(?:youtube.com|youtu.be)\/(?:v\/|embed\/|watch(?:\?v=|\/))?([a-zA-Z0-9-]+)/i, + match = url.match(regex); + + if (match){ + var token = match[1], + embedurl = "https://www.youtube.com/embed/" + token + "?html5=1&iv_load_policy=3&modestbranding=1&rel=0&showinfo=0", + element = angular.element('') + .attr('src', embedurl) + .attr('width', '560') + .attr('height', '315') + .attr('frameborder', '0') + .attr('allowfullscreen', 'true'); + return element.prop('outerHTML'); + } + }); + + /* + * Dailymotion Embedded Player + * + * See: http://www.dailymotion.com/doc/api/player.html + */ + var dailymotionPlugin = new Plugin('Dailymotion video', function(message) { + var rPath = /dailymotion.com\/.*video\/([^_?# ]+)/; + var rAnchor = /dailymotion.com\/.*#video=([^_& ]+)/; + var rShorten = /dai.ly\/([^_?# ]+)/; + + var match = message.match(rPath) || message.match(rAnchor) || message.match(rShorten); + if (match) { + var id = match[1]; + var embedurl = 'https://www.dailymotion.com/embed/video/' + id + '?html&controls=html&startscreen=html&info=0&logo=0&related=0'; + var element = angular.element('') + .attr('src', embedurl) + .attr('width', '480') + .attr('height', '270') + .attr('frameborder', '0'); + return element.prop('outerHTML'); + } + + return null; + }); + + /* + * AlloCine Embedded Player + */ + var allocinePlugin = new Plugin('AlloCine video', function(message) { + var rVideokast = /allocine.fr\/videokast\/video-(\d+)/; + var rCmedia = /allocine.fr\/.*cmedia=(\d+)/; + + var match = message.match(rVideokast) || message.match(rCmedia); + if (match) { + var id = match[1]; + var embedurl = 'http://www.allocine.fr/_video/iblogvision.aspx?cmedia=' + id; + var element = angular.element('') + .attr('src', embedurl) + .attr('width', '480') + .attr('height', '270') + .attr('frameborder', '0'); + return element.prop('outerHTML'); + } + + return null; + }); + + /* + * Image Preview + */ + var imagePlugin = new UrlPlugin('image', function(url) { + if (url.match(/\.(png|gif|jpg|jpeg)(:(small|medium|large))?\b/i)) { + /* A fukung.net URL may end by an image extension but is not a direct link. */ + if (url.indexOf("^https?://fukung.net/v/") != -1) { + url = url.replace(/.*\//, "http://media.fukung.net/imgs/"); + } else if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) { + // remove protocol specification to load over https if used by g-b + url = url.replace(/http:/, ""); + } else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\//i)) { + // Dropbox requires a get parameter, dl=1 + var dbox_url = document.createElement("a"); + dbox_url.href = url; + var base_url = dbox_url.protocol + '//' + dbox_url.host + dbox_url.pathname + '?'; + var dbox_params = dbox_url.search.substring(1).split('&'); + var dl_added = false; + for (var i = 0; i < dbox_params.length; i++) { + if (dbox_params[i].split('=')[0] === "dl") { + dbox_params[i] = "dl=1"; + dl_added = true; + // we continue looking at the other parameters in case + // it's specified twice or something + } + } + if (!dl_added) { + dbox_params.push("dl=1"); + } + url = base_url + dbox_params.join('&'); + } + return function() { + var element = this.getElement(); + var imgElem = angular.element('') + .attr('target', '_blank') + .attr('href', url) + .append(angular.element('') + .addClass('embed') + .attr('src', url)); + element.innerHTML = imgElem.prop('outerHTML'); + }; + } + }); + + /* + * audio Preview + */ + var audioPlugin = new UrlPlugin('audio', function(url) { + if (url.match(/\.(mp3|ogg|wav)\b/i)) { + return function() { + var element = this.getElement(); + var aelement = angular.element('') + .addClass('embed') + .attr('width', '560') + .append(angular.element('') + .attr('src', url)); + element.innerHTML = aelement.prop('outerHTML'); + }; + } + }); + + + /* + * mp4 video Preview + */ + var videoPlugin = new UrlPlugin('video', function(url) { + if (url.match(/\.(mp4|webm|ogv|gifv)\b/i)) { + if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) { + // remove protocol specification to load over https if used by g-b + url = url.replace(/\.(gifv)\b/i, ".webm"); + } + return function() { + var element = this.getElement(); + var velement = angular.element('') + .addClass('embed') + .attr('width', '560') + .append(angular.element('') + .attr('src', url)); + element.innerHTML = velement.prop('outerHTML'); + }; + } + }); + + + /* + * Cloud Music Embedded Players + */ + var cloudmusicPlugin = new UrlPlugin('cloud music', function(url) { + /* SoundCloud http://help.soundcloud.com/customer/portal/articles/247785-what-widgets-can-i-use-from-soundcloud- */ + var element; + if (url.match(/^https?:\/\/soundcloud.com\//)) { + element = angular.element('') + .attr('width', '100%') + .attr('height', '120') + .attr('scrolling', 'no') + .attr('frameborder', 'no') + .attr('src', 'https://w.soundcloud.com/player/?url=' + url + '&color=ff6600&auto_play=false&show_artwork=true'); + return element.prop('outerHTML'); + } + + /* MixCloud */ + if (url.match(/^https?:\/\/([a-z]+\.)?mixcloud.com\//)) { + element = angular.element('') + .attr('width', '480') + .attr('height', '60') + .attr('frameborder', '0') + .attr('src', '//www.mixcloud.com/widget/iframe/?feed=' + url + '&mini=1&stylecolor=&hide_artwork=&embed_type=widget_standard&hide_tracklist=1&hide_cover='); + return element.prop('outerHTML'); + } + }); + + /* + * Google Maps + */ + var googlemapPlugin = new UrlPlugin('Google Map', function(url) { + if (url.match(/^https?:\/\/maps\.google\./i) || url.match(/^https?:\/\/(?:[\w]+\.)?google\.[\w]+\/maps/i)) { + var element = angular.element('') + .attr('width', '450') + .attr('height', '350') + .attr('frameborder', '0') + .attr('scrolling', 'no') + .attr('marginheight', '0') + .attr('src', url + '&output=embed'); + return element.prop('outerHTML'); + } + }); + + /* + * Asciinema plugin + */ + var asciinemaPlugin = new UrlPlugin('ascii cast', function(url) { + var regexp = /^https?:\/\/(?:www\.)?asciinema.org\/a\/(\d+)/i, + match = url.match(regexp); + if (match) { + var id = match[1]; + return function() { + var element = this.getElement(); + var scriptElem = document.createElement('script'); + scriptElem.src = 'https://asciinema.org/a/' + id + '.js'; + scriptElem.id = 'asciicast-' + id; + scriptElem.async = true; + element.appendChild(scriptElem); + }; + } + }); + + var yrPlugin = new UrlPlugin('meteogram', function(url) { + var regexp = /^https?:\/\/(?:www\.)?yr\.no\/(place|stad|sted|sadji|paikka)\/(([^\s.;,(){}<>\/]+\/){3,})/; + var match = url.match(regexp); + if (match) { + return function() { + var element = this.getElement(); + var language = match[1]; + var location = match[2]; + var city = match[match.length - 1].slice(0, -1); + url = "http://www.yr.no/" + language + "/" + location + "avansert_meteogram.png"; + var ielement = angular.element('') + .attr('src', url) + .attr('alt', 'Meteogram for ' + city); + element.innerHTML = ielement.prop('outerHTML'); + }; + } + }); + + // Embed GitHub gists + var gistPlugin = new UrlPlugin('Gist', function(url) { + var regexp = /^https:\/\/gist\.github.com\/[^.?]+/i; + var match = url.match(regexp); + if (match) { + // get the URL from the match to trim away pseudo file endings and request parameters + url = match[0] + '.json'; + // load gist asynchronously -- return a function here + return function() { + var element = this.getElement(); + jsonp(url, function(data) { + // Add the gist stylesheet only once + if (document.querySelectorAll('link[rel=stylesheet][href="' + data.stylesheet + '"]').length < 1) { + var stylesheet = ''; + document.getElementsByTagName('head')[0].innerHTML += stylesheet; + } + element.innerHTML = '
    ' + data.div + '
    '; + }); + }; + } + }); + + /* match giphy links and display the assocaited gif images + * sample input: http://giphy.com/gifs/eyes-shocked-bird-feqkVgjJpYtjy + * sample output: https://media.giphy.com/media/feqkVgjJpYtjy/giphy.gif + */ + var giphyPlugin = new UrlPlugin('Giphy', function(url) { + var regex = /^https?:\/\/giphy.com\/gifs\/.*-(.*)\/?/i; + // on match, id will contain the entire url in [0] and the giphy id in [1] + var id = url.match(regex); + if (id) { + var src = "https://media.giphy.com/media/" + id[1] + "/giphy.gif"; + return function() { + var element = this.getElement(); + var gelement = angular.element('') + .attr('target', '_blank') + .attr('href', url) + .append(angular.element('') + .addClass('embed') + .attr('src', src)); + element.innerHTML = gelement.prop('outerHTML'); + }; + } + }); + + var tweetPlugin = new UrlPlugin('Tweet', function(url) { + var regexp = /^https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/i; + var match = url.match(regexp); + if (match) { + url = 'https://api.twitter.com/1/statuses/oembed.json?id=' + match[2]; + return function() { + var element = this.getElement(); + jsonp(url, function(data) { + // separate the HTML into content and script tag + var scriptIndex = data.html.indexOf("'; + } + }); + + return { + plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, audioPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, giphyPlugin, tweetPlugin, vinePlugin] + }; + + +}); +})(); diff --git a/sources/js/settings.js b/sources/js/settings.js new file mode 100644 index 0000000..4e19ec9 --- /dev/null +++ b/sources/js/settings.js @@ -0,0 +1,82 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat'); + +weechat.factory('settings', ['$store', '$rootScope', function($store, $rootScope) { + var that = this; + this.callbacks = {}; + // This cache is important for two reasons. One, angular hits it up really often + // (because it needs to check for changes and it's not very clever about it). + // Two, it prevents weird type conversion issues that otherwise arise in + // $store.parseValue (e.g. converting "123." to the number 123 even though it + // actually was the beginning of an IP address that the user was in the + // process of entering) + this.cache = {}; + + // Define a property for a setting, retrieving it on read + // and writing it to localStorage on write + var defineProperty = function(key) { + Object.defineProperty(that, key, { + enumerable: true, + key: key, + get: function() { + if (!(key in this.cache)) { + this.cache[key] = $store.get(key); + } + return this.cache[key]; + }, + set: function(newVal) { + this.cache[key] = newVal; + $store.set(key, newVal); + // Call any callbacks + var callbacks = that.callbacks[key]; + for (var i = 0; callbacks !== undefined && i < callbacks.length; i++) { + callbacks[i](newVal); + } + // Update the page (might be needed) + setTimeout(function() { + $rootScope.$apply(); + }, 0); + } + }); + }; + + // Define properties for all settings + var keys = $store.enumerateKeys(); + for (var keyIdx in keys) { + var key = keys[keyIdx]; + defineProperty(key); + } + + // Add a callback to be called whenever the value is changed + // It's like a free $watch and used to be called the observer + // pattern, but I guess that's too old-school for JS kids :> + this.addCallback = function(key, callback, callNow) { + if (this.callbacks[key] === undefined) { + this.callbacks[key] = [callback]; + } else { + this.callbacks[key].push(callback); + } + // call now to emulate $watch behaviour + setTimeout(function() { + callback($store.get(key)); + }, 0); + }; + + this.setDefaults = function(defaults) { + for (var key in defaults) { + // null means the key isn't set + if ($store.get(key) === null) { + // Define property so it will get saved to store + defineProperty(key); + // Save to settings module AND to store + this[key] = defaults[key]; + } + } + }; + + return this; +}]); + +})(); diff --git a/sources/js/utils.js b/sources/js/utils.js new file mode 100644 index 0000000..5a10c7a --- /dev/null +++ b/sources/js/utils.js @@ -0,0 +1,29 @@ +var weechat = angular.module('weechat'); + +weechat.factory('utils', function() { + // Helper to change style of a class + var changeClassStyle = function(classSelector, attr, value) { + _.each(document.getElementsByClassName(classSelector), function(e) { + e.style[attr] = value; + }); + }; + // Helper to get style from a class + var getClassStyle = function(classSelector, attr) { + _.each(document.getElementsByClassName(classSelector), function(e) { + return e.style[attr]; + }); + }; + + var isMobileUi = function() { + // TODO don't base detection solely on screen width + // You are right. In the meantime I am renaming isMobileDevice to isMobileUi + var mobile_cutoff = 968; + return (document.body.clientWidth < mobile_cutoff); + }; + + return { + changeClassStyle: changeClassStyle, + getClassStyle: getClassStyle, + isMobileUi: isMobileUi + }; +}); diff --git a/sources/js/websockets.js b/sources/js/websockets.js new file mode 100644 index 0000000..f7c632b --- /dev/null +++ b/sources/js/websockets.js @@ -0,0 +1,150 @@ +(function() { +'use strict'; + +var websockets = angular.module('ngWebsockets', []); + +websockets.factory('ngWebsockets', + ['$rootScope','$q', +function($rootScope, $q) { + + + var protocol = null; + + var ws = null; + var callbacks = {}; + var currentCallBackId = 0; + + /* + * Fails every currently subscribed callback for the + * given reason + * + * @param reason reason for failure + */ + var failCallbacks = function(reason) { + for (var i in callbacks) { + callbacks[i].cb.reject(reason); + } + + }; + + + /* + * Returns the current callback id + */ + var getCurrentCallBackId = function() { + + currentCallBackId += 1; + + if (currentCallBackId > 1000) { + currentCallBackId = 0; + } + + return currentCallBackId; + }; + + + /* Send a message to the websocket and returns a promise. + * See: http://docs.angularjs.org/api/ng.$q + * + * @param message message to send + * @returns a promise + */ + var send = function(message) { + + var cb = createCallback(message); + + message = protocol.setId(cb.id, + message); + + ws.send(message); + return cb.promise; + }; + + /* + * Create a callback, adds it to the callback list + * and return it. + */ + var createCallback = function() { + var defer = $q.defer(); + var cbId = getCurrentCallBackId(); + + callbacks[cbId] = { + time: new Date(), + cb: defer + }; + + defer.id = cbId; + + return defer; + }; + + /* + * Send all messages to the websocket and returns a promise that is resolved + * when all message are resolved. + * + * @param messages list of messages + * @returns a promise + */ + var sendAll = function(messages) { + var promises = []; + for (var i in messages) { + var promise = send(messages[i]); + promises.push(promise); + } + return $q.all(promises); + }; + + + var onmessage = function (evt) { + /* + * Receives a message on the websocket + */ + var message = protocol.parse(evt.data); + if (_.has(callbacks, message.id)) { + // see if it's bound to one of the callbacks + var promise = callbacks[message.id]; + promise.cb.resolve(message); + delete(callbacks[message.id]); + } else { + // otherwise emit it + $rootScope.$emit('onMessage', message); + } + // Make sure all UI is updated with new data + $rootScope.$apply(); + + }; + + var connect = function(url, + protocol_, + properties) { + + ws = new WebSocket(url); + protocol = protocol_; + for (var property in properties) { + ws[property] = properties[property]; + } + + if ('onmessage' in properties) { + ws.onmessage = function(event) { + properties.onmessage(event); + onmessage(event); + }; + } else { + ws.onmessage = onmessage; + } + }; + + var disconnect = function() { + ws.close(); + }; + + return { + send: send, + sendAll: sendAll, + connect: connect, + disconnect: disconnect, + failCallbacks: failCallbacks + }; + +}]); +})(); diff --git a/sources/js/weechat.js b/sources/js/weechat.js new file mode 100644 index 0000000..640ccbd --- /dev/null +++ b/sources/js/weechat.js @@ -0,0 +1,1284 @@ +(function(exports) {// http://weechat.org/files/doc/devel/weechat_dev.en.html#color_codes_in_strings +'use strict'; + +/** + * WeeChat protocol handling. + * + * This object parses messages and formats commands for the WeeChat + * protocol. It's independent from the communication layer and thus + * may be used with any network mechanism. + */ +(function() { + var WeeChatProtocol = function() { + // specific parsing for each object type + this._types = { + 'chr': this._getChar, + 'int': this._getInt, + 'str': this._getString, + 'inf': this._getInfo, + 'hda': this._getHdata, + 'ptr': this._getPointer, + 'lon': this._getStrNumber, + 'tim': this._getTime, + 'buf': this._getString, + 'arr': this._getArray, + 'htb': this._getHashTable, + 'inl': this._getInfolist, + }; + + // string value for some object types + this._typesStr = { + 'chr': this._strDirect, + 'str': this._strDirect, + 'int': this._strToString, + 'tim': this._strToString, + 'ptr': this._strDirect + }; + }; + + /** + * WeeChat colors names. + */ + WeeChatProtocol._weeChatColorsNames = [ + 'default', + 'black', + 'darkgray', + 'red', + 'lightred', + 'green', + 'lightgreen', + 'brown', + 'yellow', + 'blue', + 'lightblue', + 'magenta', + 'lightmagenta', + 'cyan', + 'lightcyan', + 'gray', + 'white' + ]; + + /** + * Style options names. + */ + WeeChatProtocol._colorsOptionsNames = [ + 'separator', + 'chat', + 'chat_time', + 'chat_time_delimiters', + 'chat_prefix_error', + 'chat_prefix_network', + 'chat_prefix_action', + 'chat_prefix_join', + 'chat_prefix_quit', + 'chat_prefix_more', + 'chat_prefix_suffix', + 'chat_buffer', + 'chat_server', + 'chat_channel', + 'chat_nick', + 'chat_nick_self', + 'chat_nick_other', + 'invalid', + 'invalid', + 'invalid', + 'invalid', + 'invalid', + 'invalid', + 'invalid', + 'invalid', + 'invalid', + 'invalid', + 'chat_host', + 'chat_delimiters', + 'chat_highlight', + 'chat_read_marker', + 'chat_text_found', + 'chat_value', + 'chat_prefix_buffer', + 'chat_tags', + 'chat_inactive_window', + 'chat_inactive_buffer', + 'chat_prefix_buffer_inactive_buffer', + 'chat_nick_offline', + 'chat_nick_offline_highlight', + 'chat_nick_prefix', + 'chat_nick_suffix', + 'emphasis', + 'chat_day_change' + ]; + + /** + * Gets the default color. + * + * @return Default color + */ + WeeChatProtocol._getDefaultColor = function() { + return { + type: 'weechat', + name: 'default' + }; + }; + + /** + * Gets the default attributes. + * + * @return Default attributes + */ + WeeChatProtocol._getDefaultAttributes = function() { + return { + name: null, + override: { + 'bold': false, + 'reverse': false, + 'italic': false, + 'underline': false + } + }; + }; + + /** + * Gets the default style (default colors and attributes). + * + * @return Default style + */ + WeeChatProtocol._getDefaultStyle = function() { + return { + fgColor: WeeChatProtocol._getDefaultColor(), + bgColor: WeeChatProtocol._getDefaultColor(), + attrs: WeeChatProtocol._getDefaultAttributes() + }; + }; + + /** + * Clones a color object. + * + * @param color Color object to clone + * @return Cloned color object + */ + WeeChatProtocol._cloneColor = function(color) { + var clone = {}; + + for (var key in color) { + clone[key] = color[key]; + } + + return clone; + }; + + /** + * Clones an attributes object. + * + * @param attrs Attributes object to clone + * @return Cloned attributes object + */ + WeeChatProtocol._cloneAttrs = function(attrs) { + var clone = {}; + + clone.name = attrs.name; + clone.override = {}; + for (var attr in attrs.override) { + clone.override[attr] = attrs.override[attr]; + } + + return clone; + }; + + /** + * Gets the name of an attribute from its character. + * + * @param ch Character of attribute + * @return Name of attribute + */ + WeeChatProtocol._attrNameFromChar = function(ch) { + var chars = { + // WeeChat protocol + '*': 'b', + '!': 'r', + '/': 'i', + '_': 'u', + + // some extension often used (IRC?) + '\x01': 'b', + '\x02': 'r', + '\x03': 'i', + '\x04': 'u' + }; + + if (ch in chars) { + return chars[ch]; + } + + return null; + }; + + + /** + * Gets an attributes object from a string of attribute characters. + * + * @param str String of attribute characters + * @return Attributes object (null if unchanged) + */ + WeeChatProtocol._attrsFromStr = function(str) { + var attrs = WeeChatProtocol._getDefaultAttributes(); + + for (var i = 0; i < str.length; ++i) { + var ch = str.charAt(i); + if (ch === '|') { + // means keep attributes, so unchanged + return null; + } + var attrName = WeeChatProtocol._attrNameFromChar(ch); + if (attrName !== null) { + attrs.override[attrName] = true; + } + } + + return attrs; + }; + + /** + * Gets a single color from a string representing its index (WeeChat and + * extended colors only, NOT colors options). + * + * @param str Color string (e.g., "05" or "00134") + * @return Color object + */ + WeeChatProtocol._getColorObj = function(str) { + if (str.length === 2) { + var code = parseInt(str); + if (code > 16) { + // should never happen + return WeeChatProtocol._getDefaultColor(); + } else { + return { + type: 'weechat', + name: WeeChatProtocol._weeChatColorsNames[code] + }; + } + } else { + var codeStr = str.substring(1); + return { + type: 'ext', + name: parseInt(codeStr).toString() + }; + } + }; + + /** + * Gets colors and attributes of text element. + * + * See . + * + * @param txt Text element + * @return Colors, attributes and plain text of this text element: + * fgColor: Foreground color (null if unchanged) + * bgColor: Background color (null if unchanged) + * attrs: Attributes (null if unchanged) + * text: Plain text element + */ + WeeChatProtocol._getStyle = function(txt) { + var matchers = [ + { + // color option + // STD + regex: /^(\d{2})/, + fn: function(m) { + var ret = {}; + var optionCode = parseInt(m[1]); + + if (optionCode >= WeeChatProtocol._colorsOptionsNames.length) { + // should never happen + return { + fgColor: null, + bgColor: null, + attrs: null + }; + } + var optionName = WeeChatProtocol._colorsOptionsNames[optionCode]; + ret.fgColor = { + type: 'option', + name: optionName + }; + ret.bgColor = WeeChatProtocol._cloneColor(ret.fgColor); + ret.attrs = { + name: optionName, + override: {} + }; + + return ret; + } + }, + { + // ncurses pair + // EXT + regex: /^@(\d{5})/, + fn: function(m) { + // unimplemented case + return { + fgColor: null, + bgColor: null, + attrs: null + }; + } + }, + { + // foreground color with F + // "F" + (A)STD + // "F" + (A)EXT + regex: /^F(?:([*!\/_|]*)(\d{2})|@([\x01\x02\x03\x04*!\/_|]*)(\d{5}))/, + fn: function(m) { + var ret = { + bgColor: null + }; + + if (m[2]) { + ret.attrs = WeeChatProtocol._attrsFromStr(m[1]); + ret.fgColor = WeeChatProtocol._getColorObj(m[2]); + } else { + ret.attrs = WeeChatProtocol._attrsFromStr(m[3]); + ret.fgColor = WeeChatProtocol._getColorObj(m[4]); + } + + return ret; + } + }, + { + // background color (no attributes) + // "B" + STD + // "B" + EXT + regex: /^B(\d{2}|@\d{5})/, + fn: function(m) { + return { + fgColor: null, + bgColor: WeeChatProtocol._getColorObj(m[1]), + attrs: null + }; + } + }, + { + // foreground, background (+ attributes) + // "*" + (A)STD + "," + STD + // "*" + (A)STD + "," + EXT + // "*" + (A)EXT + "," + STD + // "*" + (A)EXT + "," + EXT + regex: /^\*(?:([\x01\x02\x03\x04*!\/_|]*)(\d{2})|@([\x01\x02\x03\x04*!\/_|]*)(\d{5})),(\d{2}|@\d{5})/, + fn: function(m) { + var ret = {}; + + if (m[2]) { + ret.attrs = WeeChatProtocol._attrsFromStr(m[1]); + ret.fgColor = WeeChatProtocol._getColorObj(m[2]); + } else { + ret.attrs = WeeChatProtocol._attrsFromStr(m[3]); + ret.fgColor = WeeChatProtocol._getColorObj(m[4]); + } + ret.bgColor = WeeChatProtocol._getColorObj(m[5]); + + return ret; + } + }, + { + // foreground color with * (+ attributes) (fall back, must be checked before previous case) + // "*" + (A)STD + // "*" + (A)EXT + regex: /^\*([\x01\x02\x03\x04*!\/_|]*)(\d{2}|@\d{5})/, + fn: function(m) { + return { + fgColor: WeeChatProtocol._getColorObj(m[2]), + bgColor: null, + attrs: WeeChatProtocol._attrsFromStr(m[1]) + }; + } + }, + { + // emphasis + // "E" + regex: /^E/, + fn: function(m) { + var ret = {}; + + ret.fgColor = { + type: 'option', + name: 'emphasis' + }; + ret.bgColor = WeeChatProtocol._cloneColor(ret.fgColor); + ret.attrs = { + name: 'emphasis', + override: {} + }; + + return ret; + } + } + ]; + + // parse + var ret = { + fgColor: null, + bgColor: null, + attrs: null, + text: txt + }; + matchers.some(function(matcher) { + var m = txt.match(matcher.regex); + if (m) { + ret = matcher.fn(m); + ret.text = txt.substring(m[0].length); + return true; + } + + return false; + }); + + return ret; + }; + + /** + * Transforms a raw text into an array of text elements with integrated + * colors and attributes. + * + * @param rawText Raw text to transform + * @return Array of text elements + */ + WeeChatProtocol.rawText2Rich = function(rawText) { + /* This is subtle, but JavaScript adds the token to the output list + * when it's surrounded by capturing parentheses. + */ + var parts = rawText.split(/(\x19|\x1a|\x1b|\x1c)/); + + // no colors/attributes + if (parts.length === 1) { + return [ + { + attrs: WeeChatProtocol._getDefaultAttributes(), + fgColor: WeeChatProtocol._getDefaultColor(), + bgColor: WeeChatProtocol._getDefaultColor(), + text: parts[0] + } + ]; + } + + // find the style of every part + var curFgColor = WeeChatProtocol._getDefaultColor(); + var curBgColor = WeeChatProtocol._getDefaultColor(); + var curAttrs = WeeChatProtocol._getDefaultAttributes(); + var curSpecialToken = null; + var curAttrsOnlyFalseOverrides = true; + + return parts.map(function(p) { + if (p.length === 0) { + return null; + } + var firstCharCode = p.charCodeAt(0); + var firstChar = p.charAt(0); + + if (firstCharCode >= 0x19 && firstCharCode <= 0x1c) { + // special token + if (firstCharCode === 0x1c) { + // always reset colors + curFgColor = WeeChatProtocol._getDefaultColor(); + curBgColor = WeeChatProtocol._getDefaultColor(); + if (curSpecialToken !== 0x19) { + // also reset attributes + curAttrs = WeeChatProtocol._getDefaultAttributes(); + } + } + curSpecialToken = firstCharCode; + return null; + } + + var text = p; + if (curSpecialToken === 0x19) { + // get new style + var style = WeeChatProtocol._getStyle(p); + + // set foreground color if changed + if (style.fgColor !== null) { + curFgColor = style.fgColor; + } + + // set background color if changed + if (style.bgColor !== null) { + curBgColor = style.bgColor; + } + + // set attibutes if changed + if (style.attrs !== null) { + curAttrs = style.attrs; + } + + // set plain text + text = style.text; + } else if (curSpecialToken === 0x1a || curSpecialToken === 0x1b) { + // set/reset attribute + var orideVal = (curSpecialToken === 0x1a); + + // set attribute override if we don't have to keep all of them + if (firstChar !== '|') { + var orideName = WeeChatProtocol._attrNameFromChar(firstChar); + if (orideName) { + // known attribute + curAttrs.override[orideName] = orideVal; + text = p.substring(1); + } + } + } + + // reset current special token + curSpecialToken = null; + + // if text is empty, don't bother returning it + if (text.length === 0) { + return null; + } + + /* As long as attributes are only false overrides, without any option + * name, it's safe to remove them. + */ + if (curAttrsOnlyFalseOverrides && curAttrs.name === null) { + var allReset = true; + for (var attr in curAttrs.override) { + if (curAttrs.override[attr]) { + allReset = false; + break; + } + } + if (allReset) { + curAttrs.override = {}; + } else { + curAttrsOnlyFalseOverrides = false; + } + } + + // parsed text element + return { + fgColor: WeeChatProtocol._cloneColor(curFgColor), + bgColor: WeeChatProtocol._cloneColor(curBgColor), + attrs: WeeChatProtocol._cloneAttrs(curAttrs), + text: text + }; + }).filter(function(p) { + return p !== null; + }); + }; + + /** + * Unsigned integer array to string. + * + * @param uia Unsigned integer array + * @return Decoded string + */ + WeeChatProtocol._uia2s = function(uia) { + if(!uia.length || uia[0] === 0) return ""; + + try { + var encodedString = String.fromCharCode.apply(null, uia), + decodedString = decodeURIComponent(escape(encodedString)); + return decodedString; + } catch (exception) { + // Replace all non-ASCII bytes with "?" if the string couldn't be + // decoded as UTF-8. + var s = ""; + for (var i = 0, n = uia.length; i < n; i++) { + s += uia[i] < 0x80 ? String.fromCharCode(uia[i]) : "?"; + } + return s; + } + }; + + /** + * Merges default parameters with overriding parameters. + * + * @param defaults Default parameters + * @param override Overriding parameters + * @return Merged parameters + */ + WeeChatProtocol._mergeParams = function(defaults, override) { + for (var v in override) { + defaults[v] = override[v]; + } + + return defaults; + }; + + /** + * Formats a command. + * + * @param id Command ID (null for no ID) + * @param name Command name + * @param parts Command parts + * @return Formatted command string + */ + WeeChatProtocol._formatCmd = function(id, name, parts) { + var cmdIdName; + var cmd; + + cmdIdName = (id !== null) ? '(' + id + ') ' : ''; + cmdIdName += name; + parts.unshift(cmdIdName); + cmd = parts.join(' '); + cmd += '\n'; + + cmd.replace(/[\r\n]+$/g, "").split("\n"); + + return cmd; + }; + + /** + * Formats an init command. + * + * @param params Parameters: + * password: password (optional) + * compression: compression ('off' or 'zlib') (optional) + * @return Formatted init command string + */ + WeeChatProtocol.formatInit = function(params) { + var defaultParams = { + password: null, + compression: 'zlib' + }; + var keys = []; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + keys.push('compression=' + params.compression); + if (params.password !== null) { + keys.push('password=' + params.password); + } + parts.push(keys.join(',')); + + return WeeChatProtocol._formatCmd(null, 'init', parts); + }; + + /** + * Formats an hdata command. + * + * @param params Parameters: + * id: command ID (optional) + * path: hdata path (mandatory) + * keys: array of keys (optional) + * @return Formatted hdata command string + */ + WeeChatProtocol.formatHdata = function(params) { + var defaultParams = { + id: null, + keys: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + parts.push(params.path); + if (params.keys !== null) { + parts.push(params.keys.join(',')); + } + + return WeeChatProtocol._formatCmd(params.id, 'hdata', parts); + }; + + /** + * Formats an info command. + * + * @param params Parameters: + * id: command ID (optional) + * name: info name (mandatory) + * @return Formatted info command string + */ + WeeChatProtocol.formatInfo = function(params) { + var defaultParams = { + id: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + parts.push(params.name); + + return WeeChatProtocol._formatCmd(params.id, 'info', parts); + }; + + /** + * Formats an infolist command. + * + * @param params Parameters: + * id: command ID (optional) + * name: infolist name (mandatory) + * pointer: optional + * arguments: optional + * @return Formatted infolist command string + */ + WeeChatProtocol.formatInfolist = function(params) { + var defaultParams = { + id: null, + pointer: null, + args: null + + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + parts.push(params.name); + if (params.pointer !== null) { + parts.push(params.pointer); + } + if (params.pointer !== null) { + parts.push(params.args); + } + + return WeeChatProtocol._formatCmd(params.id, 'infolist', parts); + }; + + /** + * Formats a nicklist command. + * + * @param params Parameters: + * id: command ID (optional) + * buffer: buffer name (optional) + * @return Formatted nicklist command string + */ + WeeChatProtocol.formatNicklist = function(params) { + var defaultParams = { + id: null, + buffer: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + if (params.buffer !== null) { + parts.push(params.buffer); + } + + return WeeChatProtocol._formatCmd(params.id, 'nicklist', parts); + }; + + /** + * Formats an input command. + * + * @param params Parameters: + * id: command ID (optional) + * buffer: target buffer (mandatory) + * data: input data (mandatory) + * @return Formatted input command string + */ + WeeChatProtocol.formatInput = function(params) { + var defaultParams = { + id: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + parts.push(params.buffer); + parts.push(params.data); + + return WeeChatProtocol._formatCmd(params.id, 'input', parts); + }; + + /** + * Formats a sync or a desync command. + * + * @param params Parameters (see _formatSync and _formatDesync) + * @return Formatted sync/desync command string + */ + WeeChatProtocol._formatSyncDesync = function(cmdName, params) { + var defaultParams = { + id: null, + buffers: null, + options: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + if (params.buffers !== null) { + parts.push(params.buffers.join(',')); + if (params.options !== null) { + parts.push(params.options.join(',')); + } + } + + return WeeChatProtocol._formatCmd(params.id, cmdName, parts); + }; + + /** + * Formats a sync command. + * + * @param params Parameters: + * id: command ID (optional) + * buffers: array of buffers to sync (optional) + * options: array of options (optional) + * @return Formatted sync command string + */ + WeeChatProtocol.formatSync = function(params) { + return WeeChatProtocol._formatSyncDesync('sync', params); + }; + + /** + * Formats a desync command. + * + * @param params Parameters: + * id: command ID (optional) + * buffers: array of buffers to desync (optional) + * options: array of options (optional) + * @return Formatted desync command string + */ + WeeChatProtocol.formatDesync = function(params) { + return WeeChatProtocol._formatSyncDesync('desync', params); + }; + + /** + * Formats a test command. + * + * @param params Parameters: + * id: command ID (optional) + * @return Formatted test command string + */ + WeeChatProtocol.formatTest = function(params) { + var defaultParams = { + id: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + + return WeeChatProtocol._formatCmd(params.id, 'test', parts); + }; + + /** + * Formats a quit command. + * + * @return Formatted quit command string + */ + WeeChatProtocol.formatQuit = function() { + return WeeChatProtocol._formatCmd(null, 'quit', []); + }; + + /** + * Formats a ping command. + * + * @param params Parameters: + * id: command ID (optional) + * args: array of custom arguments (optional) + * @return Formatted ping command string + */ + WeeChatProtocol.formatPing = function(params) { + var defaultParams = { + id: null, + args: null + }; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + if (params.args !== null) { + parts.push(params.args.join(' ')); + } + + return WeeChatProtocol._formatCmd(params.id, 'ping', parts); + }; + + WeeChatProtocol.prototype = { + /** + * Warns that message parsing is not implemented for a + * specific type. + * + * @param type Message type to display + */ + _warnUnimplemented: function(type) { + console.log('Warning: ' + type + ' message parsing is not implemented'); + }, + + /** + * Reads a 3-character message type token value from current + * set data. + * + * @return Type + */ + _getType: function() { + var t = this._getSlice(3); + + if (!t) { + return null; + } + + return WeeChatProtocol._uia2s(new Uint8Array(t)); + }, + + /** + * Runs the appropriate read routine for the specified message type. + * + * @param type Message type + * @return Data value + */ + _runType: function(type) { + var cb = this._types[type]; + var boundCb = cb.bind(this); + + return boundCb(); + }, + + /** + * Reads a "number as a string" token value from current set data. + * + * @return Number as a string + */ + _getStrNumber: function() { + var len = this._getByte(); + var str = this._getSlice(len); + + return WeeChatProtocol._uia2s(new Uint8Array(str)); + }, + + /** + * Returns the passed object. + * + * @param obj Object + * @return Passed object + */ + _strDirect: function(obj) { + return obj; + }, + + /** + * Calls toString() on the passed object and returns the value. + * + * @param obj Object to call toString() on + * @return String value of object + */ + _strToString: function(obj) { + return obj.toString(); + }, + + /** + * Gets the string value of an object representing the message + * value for a specified type. + * + * @param obj Object for which to get the string value + * @param type Message type + * @return String value of object + */ + _objToString: function(obj, type) { + var cb = this._typesStr[type]; + var boundCb = cb.bind(this); + + return boundCb(obj); + }, + + /** + * Reads an info token value from current set data. + * + * @return Info object + */ + _getInfo: function() { + var info = {}; + info.key = this._getString(); + info.value = this._getString(); + + return info; + }, + + /** + * Reads an hdata token value from current set data. + * + * @return Hdata object + */ + _getHdata: function() { + var self = this; + var paths; + var count; + var objs = []; + var hpath = this._getString(); + + var keys = this._getString().split(','); + paths = hpath.split('/'); + count = this._getInt(); + + keys = keys.map(function(key) { + return key.split(':'); + }); + + function runType() { + var tmp = {}; + + tmp.pointers = paths.map(function(path) { + return self._getPointer(); + }); + keys.forEach(function(key) { + tmp[key[0]] = self._runType(key[1]); + }); + objs.push(tmp); + } + + for (var i = 0; i < count; i++) { + runType(); + } + + return objs; + }, + + /** + * Reads a pointer token value from current set data. + * + * @return Pointer value + */ + _getPointer: function() { + return this._getStrNumber(); + }, + + /** + * Reads a time token value from current set data. + * + * @return Time value (Date) + */ + _getTime: function() { + var str = this._getStrNumber(); + + return new Date(parseInt(str, 10) * 1000); + }, + + /** + * Reads an integer token value from current set data. + * + * @return Integer value + */ + _getInt: function() { + var parsedData = new Uint8Array(this._getSlice(4)); + + return ((parsedData[0] & 0xff) << 24) | + ((parsedData[1] & 0xff) << 16) | + ((parsedData[2] & 0xff) << 8) | + (parsedData[3] & 0xff); + }, + + /** + * Reads a byte from current set data. + * + * @return Byte value (integer) + */ + _getByte: function() { + var parsedData = new Uint8Array(this._getSlice(1)); + + return parsedData[0]; + }, + + /** + * Reads a character token value from current set data. + * + * @return Character (string) + */ + _getChar: function() { + return this._getByte(); + }, + + /** + * Reads a string token value from current set data. + * + * @return String value + */ + _getString: function() { + var l = this._getInt(); + + if (l > 0) { + var s = this._getSlice(l); + var parsedData = new Uint8Array(s); + + return WeeChatProtocol._uia2s(parsedData); + } + + return ""; + }, + + /** + * Reads a message header from current set data. + * + * @return Header object + */ + _getHeader: function() { + var len = this._getInt(); + var comp = this._getByte(); + + return { + length: len, + compression: comp + }; + }, + + /** + * Reads a message header ID from current set data. + * + * @return Message ID (string) + */ + _getId: function() { + return this._getString(); + }, + + /** + * Reads an arbitrary object token from current set data. + * + * @return Object value + */ + _getObject: function() { + var self = this; + var type = this._getType(); + + if (type) { + return { + type: type, + content: self._runType(type) + }; + } + }, + + /** + * Reads an hash table token from current set data. + * + * @return Hash table + */ + _getHashTable: function() { + var self = this; + var typeKeys, typeValues, count; + var dict = {}; + + typeKeys = this._getType(); + typeValues = this._getType(); + count = this._getInt(); + + for (var i = 0; i < count; ++i) { + var key = self._runType(typeKeys); + var keyStr = self._objToString(key, typeKeys); + var value = self._runType(typeValues); + dict[keyStr] = value; + } + + return dict; + }, + + /** + * Reads an array token from current set data. + * + * @return Array + */ + _getArray: function() { + var self = this; + var type; + var count; + var values; + + type = this._getType(); + count = this._getInt(); + values = []; + + for (var i = 0; i < count; i++) { + values.push(self._runType(type)); + } + + return values; + }, + + /** + * Reads an infolist object from the current set of data + * + * @return Array + */ + _getInfolist: function() { + var self = this; + var name; + var count; + var values; + + name = this._getString(); + count = this._getInt(); + values = []; + + for (var i = 0; i < count; i++) { + var itemcount = self._getInt(); + var litem = []; + for (var j = 0; j < itemcount; j++) { + var item = {}; + item[self._getString()] = self._runType(self._getType()); + litem.push(item); + } + values.push(litem); + } + + return values; + }, + + /** + * Reads a specified number of bytes from current set data. + * + * @param length Number of bytes to read + * @return Sliced array + */ + _getSlice: function(length) { + if (this.dataAt + length > this._data.byteLength) { + return null; + } + + var slice = this._data.slice(this._dataAt, this._dataAt + length); + + this._dataAt += length; + + return slice; + }, + + /** + * Sets the current data. + * + * @param data Current data + */ + _setData: function(data) { + this._data = data; + }, + + + /** + * Add the ID to the previously formatted command + * + * @param id Command ID + * @param command previously formatted command + */ + setId: function(id, command) { + return '(' + id + ') ' + command; + }, + + /** + * Parses a WeeChat message. + * + * @param data Message data (ArrayBuffer) + * @return Message value + */ + parse: function(data, optionsValues) { + var self = this; + + this._setData(data); + this._dataAt = 0; + + var header = this._getHeader(); + + if (header.compression) { + var raw = new Uint8Array(data, 5); // skip first five bytes (header, 4B size, 1B compression flag) + var inflate = new Zlib.Inflate(raw); + var plain = inflate.decompress(); + this._setData(plain.buffer); + this._dataAt = 0; // reset position in data, as the header is not part of the decompressed data + } + + var id = this._getId(); + var objects = []; + var object = this._getObject(); + + while (object) { + objects.push(object); + object = self._getObject(); + } + var msg = { + header: header, + id: id, + objects: objects + }; + + return msg; + } + }; + + exports.Protocol = WeeChatProtocol; +})(); +})(typeof exports === "undefined" ? this.weeChat = {} : exports); diff --git a/sources/js/whenscrolled-directive.js b/sources/js/whenscrolled-directive.js new file mode 100644 index 0000000..39a43e5 --- /dev/null +++ b/sources/js/whenscrolled-directive.js @@ -0,0 +1,21 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat'); +weechat.directive('whenScrolled', function() { + return function(scope, elm, attr) { + var raw = elm[0]; + + var fun = function() { + if (raw.scrollTop === 0) { + scope.$apply(attr.whenScrolled); + } + }; + + elm.bind('scroll', function() { + _.debounce(fun, 200)(); + }); + }; +}); + +})(); diff --git a/sources/manifest.json b/sources/manifest.json new file mode 100644 index 0000000..437ce9d --- /dev/null +++ b/sources/manifest.json @@ -0,0 +1,25 @@ +{ + "name": "Glowing Bear", + "description": "WeeChat Web frontend", + "version": "0.6.0", + "manifest_version": 2, + "icons": { + "32": "assets/img/favicon.png", + "128": "assets/img/glowing_bear_128x128.png" + }, + "app": { + "urls": [ + "http://glowing-bear.github.io/glowing-bear/" + ], + "launch": { + "container": "panel", + "web_url": "http://glowing-bear.github.io/glowing-bear/" + } + }, + "permissions": [ + "notifications" + ], + "web_accessible_resources": [ + "assets/img/favicon.png" + ] +} diff --git a/sources/manifest.webapp b/sources/manifest.webapp new file mode 100644 index 0000000..eb8cdcd --- /dev/null +++ b/sources/manifest.webapp @@ -0,0 +1,29 @@ +{ + "name": "Glowing Bear", + "description": "WeeChat Web frontend", + "launch_path": "/glowing-bear/index.html", + "icons": { + "128": "/glowing-bear/assets/img/glowing_bear_128x128.png", + "60": "/glowing-bear/assets/img/glowing_bear_60x60.png", + "90": "/glowing-bear/assets/img/glowing_bear_90x90.png", + "32": "/glowing-bear/assets/img/favicon.png" + }, + "installs_allowed_from": [ + "*" + ], + "developer": { + "name": "The Glowing Bear Authors", + "url": "https://github.com/glowing-bear" + }, + "permissions": { + "audio-channel-normal" : { + "description" : "Needed to play this app's audio content on the normal channel" + }, + "audio-channel-content" : { + "description" : "Needed to play this app's audio content on the content channel" + }, + "desktop-notification":{} + }, + "default_locale": "en", + "version": "0.6.0" +} diff --git a/sources/package.json b/sources/package.json new file mode 100644 index 0000000..4c8bea2 --- /dev/null +++ b/sources/package.json @@ -0,0 +1,36 @@ +{ + "name": "glowing-bear", + "private": true, + "version": "0.6.0", + "description": "A web client for Weechat", + "repository": "https://github.com/glowing-bear/glowing-bear", + "license": "GPLv3", + "devDependencies": { + "bower": "^1.3.1", + "http-server": "^0.6.1", + "jasmine-core": "^2.4.1", + "jshint": "^2.5.2", + "karma": "~0.13", + "karma-jasmine": "^0.3.6", + "karma-junit-reporter": "^0.2.2", + "karma-phantomjs-launcher": "^0.2.1", + "phantomjs": "^1.9.19", + "protractor": "~0.20.1", + "shelljs": "^0.2.6", + "uglify-js": "^2.4" + }, + "scripts": { + "postinstall": "bower install", + "minify": " uglifyjs js/localstorage.js js/weechat.js js/irc-utils.js js/glowingbear.js js/settings.js js/utils.js js/notifications.js js/filters.js js/handlers.js js/connection.js js/file-change.js js/imgur-drop-directive.js js/whenscrolled-directive.js js/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/plugins.js js/imgur.js -c -m --screw-ie8 -o min.js --source-map min.map", + "prestart": "npm install", + "start": "http-server -a localhost -p 8000", + "pretest": "npm install", + "test": "karma start test/karma.conf.js", + "test-single-run": "karma start test/karma.conf.js --single-run", + "preupdate-webdriver": "npm install", + "update-webdriver": "webdriver-manager update", + "preprotractor": "npm run update-webdriver", + "protractor": "protractor test/protractor-conf.js", + "update-index-async": "node -e \"require('shelljs/global'); sed('-i', /\\/\\/@@NG_LOADER_START@@[\\s\\S]*\\/\\/@@NG_LOADER_END@@/, '//@@NG_LOADER_START@@\\n' + cat('app/bower_components/angular-loader/angular-loader.min.js') + '\\n//@@NG_LOADER_END@@', 'app/index-async.html');\"" + } +} diff --git a/sources/run_tests.sh b/sources/run_tests.sh new file mode 100755 index 0000000..7ae7475 --- /dev/null +++ b/sources/run_tests.sh @@ -0,0 +1,2 @@ +./node_modules/.bin/jshint js/*.js test/unit/*.js +npm test diff --git a/sources/serviceworker.js b/sources/serviceworker.js new file mode 100644 index 0000000..7a549b6 --- /dev/null +++ b/sources/serviceworker.js @@ -0,0 +1,46 @@ +// File needs to be stored in the root of the app. + +this.addEventListener('install', function(event) { + event.waitUntil( + caches.open('v1').then(function(cache) { + return cache.addAll([ + 'assets/img/glowing_bear_128x128.png', + ]); + }) + ); +}); + +this.addEventListener('push', function(event) { + // TODO, support GCM here + var title = 'Push message'; + event.waitUntil( + self.registration.showNotification(title, { + body: 'The Message', + icon: 'assets/img/favicon.png', + tag: 'my-tag' + })); +}); + +this.onnotificationclick = function(event) { + // Android doesn't close the notification when you click on it + // See: http://crbug.com/463146 + event.notification.close(); + + // This looks to see if the current is already open and + // focuses if it is + event.waitUntil(clients.matchAll({ + type: "window" + }).then(function(clientList) { + for (var i = 0; i < clientList.length; i++) { + var client = clientList[i]; + if ('focus' in client) { + return client.focus(); + } + } + /* + if (clients.openWindow) { + return clients.openWindow('/glowing-bear/'); + } + */ + })); +}; diff --git a/sources/test/e2e/scenarios.js b/sources/test/e2e/scenarios.js new file mode 100644 index 0000000..a1efb07 --- /dev/null +++ b/sources/test/e2e/scenarios.js @@ -0,0 +1,26 @@ +'use strict'; + +/* https://github.com/angular/protractor/blob/master/docs/getting-started.md */ + +describe('Auth', function() { + + browser.get('index.html'); + var ptor = protractor.getInstance(); + it('auth should fail when trying to connect to an unused port', function() { + var host = ptor.findElement(protractor.By.model('host')); + var password = ptor.findElement(protractor.By.model('password')); + var port = ptor.findElement(protractor.By.model('port')); + var submit = ptor.findElement(protractor.By.tagName('button')); + // Fill out the form? + host.sendKeys('localhost'); + password.sendKeys('password'); + port.sendKeys(2462); + submit.click(); + + var error = ptor.findElement( + protractor.By.css('.alert.alert-danger > strong') + ) + + expect(error.getText()).toBeDefined(); + }); +}); diff --git a/sources/test/karma.conf.js b/sources/test/karma.conf.js new file mode 100644 index 0000000..66e3f04 --- /dev/null +++ b/sources/test/karma.conf.js @@ -0,0 +1,49 @@ +module.exports = function(config){ + config.set({ + + basePath : '../', + + files : [ + 'bower_components/angular/angular.js', + 'bower_components/angular-route/angular-route.js', + 'bower_components/angular-mocks/angular-mocks.js', + 'bower_components/angular-sanitize/angular-sanitize.js', + 'bower_components/angular-touch/angular-touch.js', + 'js/localstorage.js', + 'js/weechat.js', + 'js/irc-utils.js', + 'js/glowingbear.js', + 'js/utils.js', + 'js/notifications.js', + 'js/filters.js', + 'js/handlers.js', + 'js/connection.js', + 'js/inputbar.js', + 'js/plugin-directive.js', + 'js/websockets.js', + 'js/models.js', + 'js/plugins.js', + 'test/unit/**/*.js' + ], + + autoWatch : true, + + frameworks: ['jasmine'], + + browsers : ['PhantomJS'], + + singleRun: true, + + plugins : [ + 'karma-phantomjs-launcher', + 'karma-jasmine', + 'karma-junit-reporter' + ], + + junitReporter : { + outputFile: 'test_out/unit.xml', + suite: 'unit' + } + + }); +}; diff --git a/sources/test/protractor-conf.js b/sources/test/protractor-conf.js new file mode 100644 index 0000000..d5bf372 --- /dev/null +++ b/sources/test/protractor-conf.js @@ -0,0 +1,19 @@ +exports.config = { + allScriptsTimeout: 11000, + + specs: [ + 'e2e/*.js' + ], + + capabilities: { + 'browserName': 'firefox' + }, + + baseUrl: 'http://localhost:8000/', + + framework: 'jasmine', + + jasmineNodeOpts: { + defaultTimeoutInterval: 30000 + } +}; diff --git a/sources/test/unit/filters.js b/sources/test/unit/filters.js new file mode 100644 index 0000000..2b1358f --- /dev/null +++ b/sources/test/unit/filters.js @@ -0,0 +1,95 @@ +var weechat = angular.module('weechat'); + +describe('Filters', function() { + beforeEach(module('weechat')); + /*beforeEach(module(function($provide) { + $provide.value('version', 'TEST_VER'); + }));*/ + + it('has an irclinky filter', inject(function($filter) { + expect($filter('irclinky')).not.toBeNull(); + })); + + describe('irclinky', function() { + it('should not mess up text', inject(function(irclinkyFilter) { + expect(irclinkyFilter('foo')).toEqual('foo'); + })); + + it('should linkify IRC channels', inject(function(irclinkyFilter) { + expect(irclinkyFilter('#foo')).toEqual('#foo'); + })); + + it('should not mess up IRC channels surrounded by HTML entities', inject(function(irclinkyFilter) { + expect(irclinkyFilter('<"#foo">')).toEqual('<"\');">#foo">'); + })); + + it('should not touch links created by `linky`', inject(function(linkyFilter, DOMfilterFilter) { + var url = 'http://foo.bar/#baz', + link = linkyFilter(url, '_blank'), + result = DOMfilterFilter(link, 'irclinky').$$unwrapTrustedValue(); + expect(result).toEqual(link); + })); + }); + + describe('inlinecolour', function() { + it('should not mess up normal text', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('foo')).toEqual('foo'); + expect(inlinecolourFilter('test #foobar baz')).toEqual('test #foobar baz'); + })); + + it('should detect inline colours in #rrggbb format', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('#123456')).toEqual('#123456
    '); + expect(inlinecolourFilter('#aabbcc')).toEqual('#aabbcc
    '); + })); + + it('should not detect inline colours in #rgb format', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('#123')).toEqual('#123'); + expect(inlinecolourFilter('#abc')).toEqual('#abc'); + })); + + it('should detect inline colours in rgb(12,34,56) and rgba(12,34,56,0.78) format', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('rgb(1,2,3)')).toEqual('rgb(1,2,3)
    '); + expect(inlinecolourFilter('rgb(1,2,3);')).toEqual('rgb(1,2,3);
    '); + expect(inlinecolourFilter('rgba(1,2,3,0.4)')).toEqual('rgba(1,2,3,0.4)
    '); + expect(inlinecolourFilter('rgba(255,123,0,0.5);')).toEqual('rgba(255,123,0,0.5);
    '); + })); + + it('should tolerate whitespace in between numbers in rgb/rgba colours', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('rgb( 1\t, 2 , 3 )')).toEqual('rgb( 1\t, 2 , 3 )
    '); + })); + + it('should handle multiple and mixed occurrences of colour values', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('rgb(1,2,3) #123456')).toEqual('rgb(1,2,3)
    #123456
    '); + expect(inlinecolourFilter('#f00baa #123456 #234567')).toEqual('#f00baa
    #123456
    #234567
    '); + expect(inlinecolourFilter('rgba(1,2,3,0.4) foorgb(50,100,150)')).toEqual('rgba(1,2,3,0.4)
    foorgb(50,100,150)
    '); + })); + + it('should not replace HTML escaped 𞉀', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('𞉀')).toEqual('𞉀'); + })); + }); + + describe('DOMfilter', function() { + it('should run a filter on all text nodes', inject(function(DOMfilterFilter) { + var dom = 'a

    bcdefgh

    i', + expected = 'A

    BCDEFGH

    I', + result = DOMfilterFilter(dom, 'uppercase').$$unwrapTrustedValue(); + expect(result).toEqual(expected); + })); + + it('should pass additional arguments to the filter', inject(function(DOMfilterFilter) { + var dom = '1

    2

    3.14159265', + expected = '1.00

    2.00

    3.14', + result = DOMfilterFilter(dom, 'number', 2).$$unwrapTrustedValue(); + expect(result).toEqual(expected); + })); + + it('should never lock up like in bug #688', inject(function(linkyFilter, DOMfilterFilter) { + var msg = '#crash http://google.com', + linked = linkyFilter(msg), + irclinked = DOMfilterFilter(linked, 'irclinky'); + // With the bug, the DOMfilterFilter call ends up in an infinite loop. + // I.e. if we ever got this far, the bug is fixed. + })); + }); +}); diff --git a/sources/test/unit/plugins.js b/sources/test/unit/plugins.js new file mode 100644 index 0000000..34be685 --- /dev/null +++ b/sources/test/unit/plugins.js @@ -0,0 +1,169 @@ +/* plugins go here */ + +var msg = function(msg) { + return {'text': msg }; +}; + +var metadata_name = function(message) { + if (message.metadata && message.metadata[0] && message.metadata[0].name) { + return message.metadata[0].name; + } + return null; +}; + +var expectTheseMessagesToContain = function(urls, pluginType, plugins) { + for (var i = 0; i < urls.length; i++) { + expect( + metadata_name( + plugins.PluginManager.contentForMessage(msg(urls[i])) + ) + ).toEqual(pluginType); + } +}; + +describe('filter', function() { + beforeEach(module('plugins')); + + describe('Plugins', function() { + beforeEach(module(function($provide) { + $provide.value('version', 'TEST_VER'); + })); + + it('should recognize spotify tracks', inject(function(plugins) { + expectTheseMessagesToContain([ + 'spotify:track:6JEK0CvvjDjjMUBFoXShNZ', + 'https://open.spotify.com/track/6JEK0CvvjDjjMUBFoXShNZ' + ], + 'Spotify track', + plugins); + })); + + + it('should recognize youtube videos', inject(function(plugins) { + expectTheseMessagesToContain([ + 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + 'http://www.youtube.com/watch?v=dQw4w9WgXcQ', + 'http://youtu.be/J6vIS8jb6Fs', + 'https://youtu.be/J6vIS8jb6Fs', + 'http://www.youtube.com/embed/dQw4w9WgXcQ', + 'https://www.youtube.com/embed/dQw4w9WgXcQ', + ], + 'YouTube video', + plugins); + })); + + it('should recognize dailymotion videos', inject(function(plugins) { + expectTheseMessagesToContain([ + 'dailymotion.com/video/test', + 'dailymotion.com/video/#video=asdf', + 'dai.ly/sfg' + ], + 'Dailymotion video', + plugins); + })); + + it('should recognize allocine videos', inject(function(plugins) { + expectTheseMessagesToContain([ + 'allocine.fr/videokast/video-12', + 'allocine.fr/cmedia=234' + ], + 'AlloCine video', + plugins); + })); + + it('should recognize html5 videos', inject(function(plugins) { + expectTheseMessagesToContain([ + 'http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4', + 'http://www.quirksmode.org/html5/videos/big_buck_bunny.webm', + 'http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv', + ], + 'video', + plugins); + })); + + it('should recognize images', inject(function(plugins) { + expectTheseMessagesToContain([ + 'http://i.imgur.com/BTNIDBR.gif', + 'https://i.imgur.com/1LmDmct.jpg', + 'http://i.imgur.com/r4FKrnu.jpeg', + 'https://4z2.de/gb-mobile-new.png', + 'http://static.weechat.org/images/screenshots/relay/medium/glowing-bear.png', + 'http://foo.bar/baz.php?img=trololo.png&dummy=yes', + 'https://tro.lo.lo/images/rick.png?size=123x45', + 'https://pbs.twimg.com/media/B66rbCuIMAAxiFF.jpg:large', + 'https://pbs.twimg.com/media/B6OZuCYCEAEV8SA.jpg:medium' + ], + 'image', + plugins); + })); + + it('should recognize cloud music', inject(function(plugins) { + expectTheseMessagesToContain([ + 'http://soundcloud.com/', + 'https://sadf.mixcloud.com/', + ], + 'cloud music', + plugins); + })); + + it('should recognize google map', inject(function(plugins) { + expectTheseMessagesToContain([ + 'https://www.google.com/maps/@48.0034139,-74.9129088,6z', + ], + 'Google Map', + plugins); + })); + + it('should recognize google map', inject(function(plugins) { + expectTheseMessagesToContain([ + 'https://asciinema.org/a/10625', + ], + 'ascii cast', + plugins); + })); + + it('should recognize meteograms', inject(function(plugins) { + expectTheseMessagesToContain([ + 'http://www.yr.no/sted/Canada/Quebec/Montreal/', + ], + 'meteogram', + plugins); + })); + + it('should recognize gists', inject(function(plugins) { + expectTheseMessagesToContain([ + 'https://gist.github.com/lorenzhs/e8c1a7d56fa170320eb8', + 'https://gist.github.com/e8c1a7d56fa170320eb8', + ], + 'Gist', + plugins); + })); + + it('should recognize giphy gifs', inject(function(plugins) { + expectTheseMessagesToContain([ + 'https://giphy.com/gifs/eyes-shocked-bird-feqkVgjJpYtjy/', + 'http://giphy.com/gifs/funny-cat-FiGiRei2ICzzG', + ], + 'Giphy', + plugins); + })); + + it('should recognize tweets', inject(function(plugins) { + expectTheseMessagesToContain([ + 'https://twitter.com/DFB_Team_EN/statuses/488436782959448065', + ], + 'Tweet', + plugins); + })); + + it('should recognize vines', inject(function(plugins) { + expectTheseMessagesToContain([ + 'https://vine.co/v/hWh262H9HM5', + 'https://vine.co/v/hWh262H9HM5/embed', + ], + 'Vine', + plugins); + })); + + }); +}); diff --git a/sources/webapp.manifest.json b/sources/webapp.manifest.json new file mode 100644 index 0000000..8367491 --- /dev/null +++ b/sources/webapp.manifest.json @@ -0,0 +1,33 @@ +{ + "lang": "en-US", + "name": "Glowing Bear", + "short_name": "Glowing Bear", + "icons": [{ + "src": "assets/img/glowing_bear_60x60.png", + "sizes": "60x60", + "type": "image/webapp" + }, { + "src": "assets/img/glowing_bear_90x90.png", + "sizes": "90x90" + }, { + "src": "assets/img/glowing_bear_128x128.png", + "sizes": "128x128" + }], + "splash_screens": [{ + "src": "assets/img/glowing_bear_128x128.png", + "sizes": "128x128" + }], + "scope": "/glowing-bear/", + "start_url": "/glowing-bear/index.html", + "display": "standalone", + "orientation": "portrait-primary", + "theme_color": "#181818", + "background_color": "#333", + "prefer_related_applications": "false", + "chrome_related_applications": [{ + "platform": "web" + }, { + "platform": "android", + "location": "https://play.google.com/store/apps/details?id=com.glowing_bear" + }] +}