first commit
661
LICENSE
Normal file
|
@ -0,0 +1,661 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<http://www.gnu.org/licenses/>.
|
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
Glowing Bear for YunoHost
|
||||
-------------------------
|
||||
|
||||
https://www.glowing-bear.org/
|
8
conf/nginx.conf
Normal file
|
@ -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;
|
||||
}
|
55
manifest.json
Normal file
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
18
scripts/backup
Executable file
|
@ -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"
|
58
scripts/install
Executable file
|
@ -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
|
34
scripts/remove
Executable file
|
@ -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
|
35
scripts/restore
Executable file
|
@ -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
|
45
scripts/upgrade
Executable file
|
@ -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
|
7
sources/3rdparty/favico-0.3.5.min.js
vendored
Normal file
15
sources/3rdparty/inflate.min.js
vendored
Normal file
|
@ -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;n<d;++n)c[n]>a&&(a=c[n]),c[n]<b&&(b=c[n]);e=1<<a;f=new (s?Uint32Array:Array)(e);g=1;h=0;for(k=2;g<=a;){for(n=0;n<d;++n)if(c[n]===g){l=0;p=h;for(r=0;r<g;++r)l=l<<1|p&1,p>>=1;K=g<<16|n;for(r=l;r<e;r+=k)f[r]=K;++h}++g;h<<=1;k<<=1}return[f,a,b]};function u(c,d){this.g=[];this.h=32768;this.d=this.f=this.a=this.l=0;this.input=s?new Uint8Array(c):c;this.m=!1;this.i=v;this.s=!1;if(d||!(d={}))d.index&&(this.a=d.index),d.bufferSize&&(this.h=d.bufferSize),d.bufferType&&(this.i=d.bufferType),d.resize&&(this.s=d.resize);switch(this.i){case w:this.b=32768;this.c=new (s?Uint8Array:Array)(32768+this.h+258);break;case v:this.b=0;this.c=new (s?Uint8Array:Array)(this.h);this.e=this.A;this.n=this.w;this.j=this.z;break;default:throw Error("invalid inflate mode");
|
||||
}}var w=0,v=1,x={u:w,t:v};
|
||||
u.prototype.k=function(){for(;!this.m;){var c=y(this,3);c&1&&(this.m=!0);c>>>=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<P;++O)N[O]=143>=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<S;++R)Q[R]=5;var A=t(Q);function y(c,d){for(var a=c.f,b=c.d,e=c.input,f=c.a,g=e.length,h;b<d;){if(f>=g)throw Error("input buffer is broken");a|=e[f++]<<b;b+=8}h=a&(1<<d)-1;c.f=a>>>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<k&&!(f>=g);)a|=e[f++]<<b,b+=8;l=h[a&(1<<k)-1];p=l>>>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;g<a;)switch(d=T(this,c),d){case 16:for(f=3+y(this,2);f--;)b[g++]=e;break;case 17:for(f=3+y(this,3);f--;)b[g++]=0;e=0;break;case 18:for(f=11+y(this,7);f--;)b[g++]=0;e=0;break;default:e=b[g++]=d}this.q=e;return b}var a=y(c,5)+257,b=y(c,5)+1,e=y(c,4)+4,f=new (s?Uint8Array:Array)(D.length),g,h,k,l;for(l=0;l<e;++l)f[D[l]]=y(c,3);if(!s){l=e;for(e=f.length;l<e;++l)f[D[l]]=0}g=t(f);h=new (s?Uint8Array:Array)(a);k=new (s?Uint8Array:Array)(b);c.q=0;
|
||||
c.j(t(d.call(c,a,g,h)),t(d.call(c,b,g,k)))}u.prototype.j=function(c,d){var a=this.c,b=this.b;this.o=c;for(var e=a.length-258,f,g,h,k;256!==(f=T(this,c));)if(256>f)b>=e&&(this.b=b,a=this.e(),b=this.b),a[b++]=f;else{g=f-257;k=F[g];0<H[g]&&(k+=y(this,H[g]));f=T(this,d);h=J[f];0<M[f]&&(h+=y(this,M[f]));b>=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];0<H[g]&&(k+=y(this,H[g]));f=T(this,d);h=J[f];0<M[f]&&(h+=y(this,M[f]));b+k>e&&(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;a<b;++a)c[a]=e[a+32768]}this.g.push(c);this.l+=c.length;if(s)e.set(e.subarray(d,d+32768));else for(a=0;32768>a;++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=f<h.length?h.length+f:h.length<<1):e=h.length*a;s?(d=new Uint8Array(e),d.set(h)):d=h;return this.c=d};
|
||||
u.prototype.n=function(){var c=0,d=this.c,a=this.g,b,e=new (s?Uint8Array:Array)(this.l+(this.b-32768)),f,g,h,k;if(0===a.length)return s?this.c.subarray(32768,this.b):this.c.slice(32768,this.b);f=0;for(g=a.length;f<g;++f){b=a[f];h=0;for(k=b.length;h<k;++h)e[c++]=b[h]}f=32768;for(g=this.b;f<g;++f)e[c++]=d[f];this.g=[];return this.buffer=e};
|
||||
u.prototype.w=function(){var c,d=this.b;s?this.s?(c=new Uint8Array(d),c.set(this.c.subarray(0,d))):c=this.c.subarray(0,d):(this.c.length>d&&(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<g;f++)e[f]=(e[f].charCodeAt(0)&255)>>>0;b=e}for(var h=1,k=0,l=b.length,p,n=0;0<l;){p=1024<l?1024:l;l-=p;do h+=b[n++],k+=h;while(--p);h%=65521;k%=65521}if(a!==(k<<16|h)>>>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
|
8
sources/3rdparty/inflate.min.js.map
vendored
Normal file
674
sources/COPYING
Normal file
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
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
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
92
sources/README.md
Normal file
|
@ -0,0 +1,92 @@
|
|||
#A web client for WeeChat [](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.
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=com.glowing_bear"><img alt="Android app on Google Play" src="/assets/img/badge_playstore.png" /></a><a href="https://marketplace.firefox.com/app/glowing-bear/"><img alt="Firefox OS app in the Firefox Marketplace" src="/assets/img/badge_firefoxos.png" /></a>
|
||||
|
||||
##Screenshots
|
||||
|
||||
Running as Chrome application in a separate window on Windows and as Android app:
|
||||
|
||||

|
||||
|
||||
Are you good with design? We'd love your help!
|
||||

|
||||
|
||||
##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.
|
BIN
sources/assets/audio/sonar.mp3
Normal file
BIN
sources/assets/audio/sonar.ogg
Normal file
BIN
sources/assets/img/badge_firefoxos.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
sources/assets/img/badge_playstore.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
sources/assets/img/favicon.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
sources/assets/img/glowing-bear.png
Normal file
After Width: | Height: | Size: 56 KiB |
1
sources/assets/img/glowing-bear.svg
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
sources/assets/img/glowing_bear_128x128.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
sources/assets/img/glowing_bear_60x60.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
sources/assets/img/glowing_bear_90x90.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
sources/assets/img/glyphicons-halflings-white.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
17
sources/bower.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
852
sources/css/glowingbear.css
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
22
sources/css/themes/black.css
Normal file
|
@ -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;
|
||||
}
|
2126
sources/css/themes/dark.css
Normal file
2102
sources/css/themes/light.css
Normal file
14
sources/directives/input.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<form class="form form-horizontal" id="inputform" ng-submit="sendMessage()" imgur-drop>
|
||||
<div class="input-group">
|
||||
<textarea id="{{inputId}}" class="form-control favorite-font" ng-trim="false" rows="1" ng-change="inputChanged()" autocomplete="on" ng-model="command" ng-focus="hideSidebar()">
|
||||
</textarea>
|
||||
<span class="input-group-btn">
|
||||
<label class="btn btn-send-image unselectable" for="imgur-upload" title="Send image">
|
||||
<i class="glyphicon glyphicon-picture"></i>
|
||||
<input type="file" accept="image/*" multiple title="Send image" id="imgur-upload" class="imgur-upload" file-change="uploadImage($event, files)">
|
||||
</label>
|
||||
<button class="btn btn-send unselectable" title="Send"><i class="glyphicon glyphicon-send"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="imgur-upload-progress"></div>
|
||||
</form>
|
18
sources/directives/plugin.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div ng-show="plugin.visible">
|
||||
<button class="btn btn-default btn-sm pull-right unselectable"
|
||||
ng-click="hideContent()">
|
||||
Hide {{ ::plugin.name }}
|
||||
</button>
|
||||
|
||||
<div ng-bind-html="displayedContent" class="embed" ng-class="::plugin.className"></div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="plugin.visible">
|
||||
<button class="btn btn-default btn-sm pull-right unselectable"
|
||||
ng-class="::{
|
||||
'btn-warning': plugin.nsfw,
|
||||
'btn-primary': !plugin.nsfw}"
|
||||
ng-click="showContent()">
|
||||
Show {{ ::plugin.name }}
|
||||
</button>
|
||||
</div>
|
513
sources/index.html
Normal file
|
@ -0,0 +1,513 @@
|
|||
<!DOCTYPE html>
|
||||
<html ng-app="weechat" ng-cloak>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Glowing Bear">
|
||||
<meta name="theme-color" content="#181818">
|
||||
<meta http-equiv="x-dns-prefetch-control" content="off">
|
||||
<!-- https://w3c.github.io/manifest/ && https://developer.mozilla.org/en-US/docs/Web/Manifest -->
|
||||
<link rel="manifest" href="webapp.manifest.json">
|
||||
<title ng-bind-template="{{ notificationStatus }}Glowing Bear {{ pageTitle}}"></title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" media="screen" integrity="sha384-7tY7Dc2Q8WQTKGz2Fa0vC4dWQo07N4mJjKvHfIGnxuC4vPqFGFQppd9b3NWpf18/" crossorigin="anonymous">
|
||||
<link rel="shortcut icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png">
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png">
|
||||
<link rel="shortcut icon" type="image/png" href="assets/img/favicon.png" >
|
||||
<link href="css/glowingbear.css" rel="stylesheet" media="screen">
|
||||
<link href="css/themes/dark.css" rel="stylesheet" media="screen" id="themeCSS" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.min.js" integrity="sha384-r1y8TJcloKTvouxnYsi4PJAx+nHNr90ibsEn3zznzDzWBN9X3o3kbHLSgcIPtzAp" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular-route.min.js" integrity="sha384-fQQcs0/yvL0uyyzpXoTKfcQl5e9GYh7GKIft35qSjfKXSILYNI6YZOM0Ju94DY+/" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular-sanitize.min.js" integrity="sha384-79uolbJAcWnfqb2Oi/w0fEz2NdE5lvY1p+TSew6D3XC7PlZY1OGuvGBiwjZhFvOg" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular-touch.min.js" integrity="sha384-bnrVwYH8/uQCvK9n+xYQKdf1xtgSNHBYcy0djCofRUPvAt93iEhBfHlngRP/aXsg" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js" integrity="sha384-nXjwhL1LfWUDVHxQ2R0rHpbr/E6lfCFXR4kfcPHp1eLGH1dH/mZohGINd44EzEya" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/emojione/2.1.0/lib/js/emojione.min.js" integrity="sha384-pJb7FFLYTcgO7KbgirAXNIHFIKzywqq4LIcWx9cavPapYWdCH5mcYptrkpHHEkH1" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" src="3rdparty/inflate.min.js"></script>
|
||||
<script type="text/javascript" src="js/localstorage.js"></script>
|
||||
<script type="text/javascript" src="js/weechat.js"></script>
|
||||
<script type="text/javascript" src="js/irc-utils.js"></script>
|
||||
<script type="text/javascript" src="js/glowingbear.js"></script>
|
||||
<script type="text/javascript" src="js/settings.js"></script>
|
||||
<script type="text/javascript" src="js/utils.js"></script>
|
||||
<script type="text/javascript" src="js/notifications.js"></script>
|
||||
<script type="text/javascript" src="js/filters.js"></script>
|
||||
<script type="text/javascript" src="js/handlers.js"></script>
|
||||
<script type="text/javascript" src="js/connection.js"></script>
|
||||
<script type="text/javascript" src="js/file-change.js"></script>
|
||||
<script type="text/javascript" src="js/imgur-drop-directive.js"></script>
|
||||
<script type="text/javascript" src="js/whenscrolled-directive.js"></script>
|
||||
<script type="text/javascript" src="js/inputbar.js"></script>
|
||||
<script type="text/javascript" src="js/plugin-directive.js"></script>
|
||||
<script type="text/javascript" src="js/websockets.js"></script>
|
||||
<script type="text/javascript" src="js/models.js"></script>
|
||||
<script type="text/javascript" src="js/plugins.js"></script>
|
||||
<script type="text/javascript" src="js/imgur.js"></script>
|
||||
<script type="text/javascript" src="3rdparty/favico-0.3.5.min.js"></script>
|
||||
</head>
|
||||
<body ng-controller="WeechatCtrl" ng-keydown="handleKeyPress($event)" ng-keyup="handleKeyRelease($event)" ng-keypress="handleKeyPress($event)" ng-class="{'no-overflow': connected}" ng-init="init()" lang="en-US">
|
||||
<div class="alert alert-danger upload-error" ng-show="uploadError">
|
||||
<p><strong>Upload error:</strong> Image upload failed.</p>
|
||||
</div>
|
||||
<div ng-hide="connected" class="container">
|
||||
<h2>
|
||||
<img alt="logo" src="assets/img/glowing-bear.svg">
|
||||
<span>Glowing Bear</span>
|
||||
<small>WeeChat web frontend</small>
|
||||
</h2>
|
||||
<div class="alert alert-danger" ng-show="errorMessage">
|
||||
<strong>Connection error</strong> The client was unable to connect to the WeeChat relay
|
||||
</div>
|
||||
<div class="alert alert-danger" ng-show="sslError">
|
||||
<strong>Secure connection error</strong> 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!
|
||||
</div>
|
||||
<div class="alert alert-danger" ng-show="securityError">
|
||||
<strong>Secure connection error</strong> 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.
|
||||
</div>
|
||||
<div class="panel-group accordion">
|
||||
<div class="panel" data-state="active">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
|
||||
Connection settings
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseOne" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<form class="form-signin" role="form">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="host">WeeChat relay hostname and port number</label>
|
||||
<div class="input-group">
|
||||
<div class="row no-gutter">
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control favorite-font" id="host" ng-model="settings.host" placeholder="Address" autocapitalize="off">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control favorite-font" id="port" ng-model="settings.port" placeholder="Port">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="control-label" for="password">WeeChat relay password</label>
|
||||
<input type="password" class="form-control favorite-font" id="password" ng-model="password" placeholder="Password">
|
||||
<div class="alert alert-danger" ng-show="passwordError">
|
||||
Error: wrong password
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label class="control-label" for="savepassword">
|
||||
<input type="checkbox" id="savepassword" ng-model="settings.savepassword">
|
||||
Save password in your browser
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox" ng-show="settings.savepassword">
|
||||
<label class="control-label" for="autoconnect">
|
||||
<input type="checkbox" id="autoconnect" ng-model="settings.autoconnect">
|
||||
Automatically connect
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label class="control-label" for="ssl">
|
||||
<input type="checkbox" id="ssl" ng-model="settings.ssl">
|
||||
Encryption. Read instructions for help
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary" ng-click="connect()">{{ connectbutton }} <i ng-class="connectbuttonicon" class="glyphicon"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel" data-state="collapsed">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
|
||||
Usage instructions
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseTwo" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<h3>Configuring the relay</h3>
|
||||
<div>To start using glowing bear, please enable the relay plugin in your WeeChat client:
|
||||
<pre>
|
||||
/set relay.network.password yourpassword
|
||||
/relay add weechat {{ settings.port || 9001 }}
|
||||
</pre>
|
||||
<span class="label label-danger">WeeChat version 0.4.2 or higher is required.</span><br>
|
||||
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.
|
||||
<br>
|
||||
<h3>Shortcuts</h3>
|
||||
Glowing Bear has a few shortcuts:
|
||||
<ul>
|
||||
<li><kbd>ALT-n</kbd>: Toggle nicklist</li>
|
||||
<li><kbd>ALT-l</kbd>: Focus on input bar</li>
|
||||
<li><kbd>ALT-[0-9]</kbd>: Switch to buffer number N</li>
|
||||
<li><kbd>ALT-a</kbd>: Focus on next buffer with activity</li>
|
||||
<li><kbd>ALT-<</kbd>: Switch to previous active buffer</li>
|
||||
<li><kbd>ALT-g</kbd>: Focus on buffer list filter</li>
|
||||
<li><kbd>Esc-Esc</kbd>: Disconnect (double-tap)</li>
|
||||
<li>Arrow keys: Navigate history</li>
|
||||
<li><kbd>Tab</kbd>: Complete nick</li>
|
||||
<li>The following readline/emacs style keybindings can be enabled with a setting: <span title="Move cursor to beginning of line"><kbd>Ctrl-a</kbd></span>, <span title="Move cursor to te end of the line"><kbd>Ctrl-e</kbd></span>, <span title="Delete from cursor to beginning of the line"><kbd>Ctrl-u</kbd></span>, <span title="Delete from cursor to the end of the line"><kbd>Ctrl-k</kbd></span>, <span title="Delete from cursor to previous space"><kbd>Ctrl-w</kbd></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel" data-state="collapsed">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
|
||||
Encryption instructions
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseThree" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<p>If you check the encryption box, the communication between browser and WeeChat will be encrypted with TLS.</p>
|
||||
<p><strong>Note</strong>: If you are using a self-signed certificate, you have to visit <a href="https://{{ settings.host }}:{{ settings.port }}/weechat">https://{{ settings.host || 'weechathost' }}:{{ settings.port || 'relayport' }}/weechat</a> 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 <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=594502">Firefox</a> and other browsers.</p>
|
||||
<p><strong>Setup</strong>: 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 <a href="https://4z2.de/2014/07/06/weechat-trusted-relay">here</a>. 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:</p>
|
||||
<pre>
|
||||
$ 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'}}/"
|
||||
</pre>
|
||||
<p>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:</p>
|
||||
<pre>
|
||||
/set relay.network.password yourpassword
|
||||
/relay sslcertkey
|
||||
/relay add ssl.weechat {{ settings.port || 9001 }}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel" data-state="collapsed" ng-hide="isinstalled">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
|
||||
Install app
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseFour" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<p>You don't need to install anything to use this app, it should work with any modern browser. Start using it <a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">right now</a>! However, there are a few ways to improve integration with your operating system.</p>
|
||||
<h3>Mobile Applications</h3>
|
||||
<p>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</p>
|
||||
<p><a href="https://play.google.com/store/apps/details?id=com.glowing_bear"><img alt="Android app on Google Play" src="assets/img/badge_playstore.png" /></a> <a href="https://marketplace.firefox.com/app/glowing-bear/"><img alt="Firefox OS app in the Firefox Marketplace" src="assets/img/badge_firefoxos.png" /></a></p>
|
||||
<h3>Firefox Browser</h3>
|
||||
<p>If you have a recent version of Firefox you can install Glowing Bear as a Firefox app. Click the button to install.</p>
|
||||
<p><button class="btn btn-lg btn-primary" ng-click="install()">Install Firefox app <i class="glyphicon glyphicon-chevron-right"></i></button></p>
|
||||
<p>Note for self-signed certificates: Firefox does not share a certificate storage with Firefox apps, so accepting self-signed certificates is a bit tricky.</p>
|
||||
<h3>Chrome</h3>
|
||||
<p>To install Glowing Bear as an app in Chrome for Android, select <kbd>Menu - Add to home screen</kbd>. In the desktop version of Chrome, click <kbd>Menu - More tools - Create application shortcuts</kbd>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel" data-state="collapsed">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
|
||||
Get involved
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseFive" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<p>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 <a href="https://github.com/glowing-bear/glowing-bear">project page</a> on GitHub!</p>
|
||||
<p>If you're interested in contributing or simply want to say hello, head over to <strong>#glowing-bear</strong> on <strong>freenode!</strong> We won't bite, promise (-ish).</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" id="content" sidebar-state="visible" ng-show="connected">
|
||||
<div id="topbar">
|
||||
<div class="brand">
|
||||
<a href="#" ng-click="toggleSidebar()">
|
||||
<img alt="brand" src="assets/img/favicon.png" title="Connected to {{ settings.host }}:{{ settings.port}}">
|
||||
</a>
|
||||
<span class="badge" ng-show="unread > 0">{{unread}}</span>
|
||||
<span class="badge danger" ng-show="notifications > 0">{{notifications}}</span>
|
||||
|
||||
<button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button>
|
||||
</div>
|
||||
|
||||
<div class="title" title="{{activeBuffer().rtitle}}">
|
||||
<span class="desktop" ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></span>
|
||||
<span class="mobile" ng-click="showModal('topicModal')" ng-class="{'active': activeBuffer().active, 'channel': activeBuffer().type === 'channel', 'channel_hash': activeBuffer().prefix === '#', 'channel_plus': activeBuffer().prefix === '+', 'channel_ampersand': activeBuffer().prefix === '&'}">{{ activeBuffer().trimmedName || activeBuffer().fullName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="actions pull-right vertical-line-left">
|
||||
<a class="settings-toggle" ng-click="showModal('settingsModal')" title="Options menu" href="#">
|
||||
<i class="glyphicon glyphicon-cog"></i>
|
||||
</a>
|
||||
<a ng-click="disconnect()" title="Disconnect from WeeChat" href="#">
|
||||
<i class="glyphicon glyphicon-off"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sidebar" data-state="visible" ng-swipe-left="hideSidebar()" class="vertical-line">
|
||||
<ul class="nav nav-pills nav-stacked" ng-class="{'indented': (predicate === 'serverSortKey'), 'showquickkeys': showQuickKeys}">
|
||||
<li class="bufferfilter">
|
||||
<form role="form">
|
||||
<input class="form-control favorite-font" type="text" id="bufferFilter" ng-model="search" ng-keydown="handleSearchBoxKey($event)" placeholder="Search" autocomplete="off">
|
||||
</form>
|
||||
</li>
|
||||
<li class="buffer" ng-class="{'active': buffer.active, 'indent': buffer.indent, 'channel': buffer.type === 'channel', 'channel_hash': buffer.prefix === '#', 'channel_plus': buffer.prefix === '+', 'channel_ampersand': buffer.prefix === '&', 'private': buffer.type === 'private'}" ng-repeat="(key, buffer) in (filteredBuffers = (getBuffers() | toArray:'withidx' | filter:{fullName:search} | filter:hasUnread | orderBy:predicate | getBufferQuickKeys:this))">
|
||||
<a ng-click="setActiveBuffer(buffer.id)" title="{{ buffer.fullName }}" href="#">
|
||||
<span class="badge pull-right" ng-class="{'danger': buffer.notification}" ng-if="buffer.notification || buffer.unread" ng-bind="buffer.notification || buffer.unread"></span>
|
||||
<span class="buffer-quick-key">{{ buffer.$quickKey }}</span>
|
||||
<span class="buffername">{{ buffer.trimmedName || buffer.fullName }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="bufferlines" class="favorite-font" ng-swipe-right="showSidebar()" ng-swipe-left="hideSidebar()" ng-class="{'withnicklist': showNicklist}" when-scrolled="infiniteScroll()" imgur-drop>
|
||||
<div id="nicklist" ng-if="showNicklist" ng-swipe-right="closeNick()" class="vertical-line-left">
|
||||
<ul class="nicklistgroup list-unstyled" ng-repeat="group in nicklist">
|
||||
<li ng-repeat="nick in group.nicks|orderBy:'name'">
|
||||
<a ng-click="openBuffer(nick.name)"><span ng-class="::nick.prefixClasses" ng-bind="::nick.prefix"></span><span ng-class="::nick.nameClasses" ng-bind="::nick.name"></span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<table ng-class="{'notimestamp':!settings.showtimestamp,'notimestampseconds':!settings.showtimestampSeconds}">
|
||||
<tbody>
|
||||
<tr class="bufferline">
|
||||
<td ng-hide="activeBuffer().allLinesFetched" colspan="3">
|
||||
<a class="fetchmorelines btn btn-xs btn-primary" ng-click="fetchMoreLines()" ng-hide="loadingLines" href="#">Fetch more lines</a>
|
||||
<span ng-show="loadingLines">Fetching more lines <i class="glyphicon glyphicon-refresh glyphicon-spin"></i></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody ng-repeat="bufferline in bufferlines">
|
||||
<tr class="bufferline">
|
||||
<td class="time">
|
||||
<span class="date" ng-class="::{'repeated-time': bufferline.shortTime==bufferlines[$index-1].shortTime}">
|
||||
<span class="cof-chat_time cob-chat_time coa-chat_time" ng-bind="::(bufferline.date|date:'HH')"></span><span class="cof-chat_time_delimiters cob-chat_time_delimiters coa-chat_time_delimiters">:</span><span class="cof-chat_time cob-chat_time coa-chat_time" ng-bind="::(bufferline.date|date:'mm')"></span><span class="seconds"><span class="cof-chat_time_delimiters cob-chat_time_delimiters coa-chat_time_delimiters">:</span><span class="cof-chat_time cob-chat_time coa-chat_time" ng-bind="::(bufferline.date|date:'ss')"></span></span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="prefix"><a ng-click="addMention(bufferline.prefix)"><span class="hidden-bracket"><</span><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text|prefixlimit:25"></span><span class="hidden-bracket">></span></a></td><!--
|
||||
--><td class="message"><!--
|
||||
--><div ng-repeat="metadata in ::bufferline.metadata" plugin data="::metadata"></div><!--
|
||||
--><span ng-repeat="part in ::bufferline.content" class="text" ng-class="::part.classes.concat(['line-' + part.$$hashKey.replace(':','_')])" ng-bind-html="::part.text | linky:'_blank' | DOMfilter:'irclinky' | DOMfilter:'emojify':settings.enableJSEmoji | DOMfilter:'inlinecolour' | DOMfilter:'mathjax':('.line-' + part.$$hashKey.replace(':','_')):settings.enableMathjax"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="readmarker" ng-if="activeBuffer().lastSeen==$index">
|
||||
<td colspan="3">
|
||||
<hr id="readmarker">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table><span id="end-of-buffer"></span>
|
||||
</div>
|
||||
<div class="footer" ng-class="{'withnicklist': showNicklist}">
|
||||
<div input-bar input-id="sendMessage" command="command"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="soundNotification"></div>
|
||||
<div id="reconnect" class="alert alert-danger" ng-click="reconnect()" ng-show="reconnecting">
|
||||
<p><strong>Connection to WeeChat lost</strong></p>
|
||||
<i class="glyphicon glyphicon-refresh"></i>
|
||||
Reconnecting... <i class="glyphicon glyphicon-spin glyphicon-refresh"></i> <a class="btn btn-xs" ng-click="reconnect()" href="#">Click to try to reconnect now</a>
|
||||
</div>
|
||||
<div id="settingsModal" class="gb-modal" data-state="hidden">
|
||||
<div class="backdrop" ng-click="closeModal($event)"></div>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">×</button>
|
||||
<span class="pull-right version">Glowing Bear version 0.6.1-dev</span>
|
||||
<h4 class="modal-title">Settings</h4>
|
||||
<p>Settings will be stored in your browser.</p>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="">
|
||||
<li class="standard-labels">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div class="form-group">
|
||||
<label for="font" class="col-sm-3 control-label make-thinner">Preferred font</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" ng-model="settings.fontfamily" class="form-control" id="font">
|
||||
</div>
|
||||
|
||||
<label for="size" class="col-sm-1 control-label">Size</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" ng-model="settings.fontsize" class="form-control" id="size">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
<li class="standard-labels">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div class="form-group">
|
||||
<label for="theme" class="col-sm-3 control-label make-thinner">Theme</label>
|
||||
<div class="col-sm-7">
|
||||
<select id="theme" class="form-control" ng-model="settings.theme" ng-options="theme for theme in themes"></select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
|
||||
<li class="standard-labels">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div class="form-group">
|
||||
<label for="custom-css" class="col-sm-3 control-label make-thinner">Custom CSS</label>
|
||||
<div class="col-sm-7">
|
||||
<textarea id="custom-css" class="form-control" ng-model="settings.customCSS"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.onlyUnread">
|
||||
Only show buffers with unread messages
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.showtimestamp">
|
||||
Show timestamps
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<ul ng-show="settings.showtimestamp">
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.showtimestampSeconds">
|
||||
Show seconds
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.noembed">
|
||||
Hide embedded content by default<span class="text-muted settings-help">NSFW content will be hidden regardless of this choice</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.hotlistsync">
|
||||
Mark messages as read in WeeChat
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.nonicklist">
|
||||
Hide nicklist
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.orderbyserver">
|
||||
Hierarchical buffer view (order by server)
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.readlineBindings">
|
||||
Enable common readline keybindings in input bar
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.useFavico">
|
||||
Display unread count in favicon
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.soundnotification">
|
||||
Play sound on notification
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.enableJSEmoji">
|
||||
Enable non-native Emoji support <span class="text-muted settings-help">Displays Emoji characters as images. Emoji provided free by <a href="http://emojione.com">http://emojione.com</a></span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form class="form-inline" role="form">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="settings.enableMathjax">
|
||||
Enable LaTeX math rendering
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="closeModal($event)">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
<div id="topicModal" class="gb-modal" data-state="hidden">
|
||||
<div class="backdrop" ng-click="closeModal($event)"></div>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Channel topic</h4>
|
||||
<p ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="closeModal($event)">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</body>
|
||||
</html>
|
387
sources/js/connection.js
Normal file
|
@ -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
|
||||
};
|
||||
}]);
|
||||
})();
|
23
sources/js/file-change.js
Normal file
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
})();
|
205
sources/js/filters.js
Normal file
|
@ -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<a href="#" onclick="openBuffer(\'$2\');">$2</a>';
|
||||
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 <div class="colourbox" style="background-color:$2"></div>';
|
||||
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;
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
848
sources/js/glowingbear.js
Normal file
|
@ -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 <head> 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'
|
||||
});
|
||||
}
|
||||
]);
|
||||
|
||||
})();
|
451
sources/js/handlers.js
Normal file
|
@ -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
|
||||
};
|
||||
|
||||
}]);
|
||||
})();
|
49
sources/js/imgur-drop-directive.js
Normal file
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
})();
|
128
sources/js/imgur.js
Normal file
|
@ -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
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
})();
|
489
sources/js/inputbar.js
Normal file
|
@ -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)<bufferlines.clientHeight/2) {
|
||||
lines[i].scrollIntoView(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Page down -> 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;
|
||||
}
|
||||
};
|
||||
}]
|
||||
};
|
||||
});
|
||||
})();
|
228
sources/js/irc-utils.js
Normal file
|
@ -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
|
||||
};
|
||||
}]);
|
||||
})();
|
117
sources/js/localstorage.js
Normal file
|
@ -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;
|
||||
}]);
|
||||
})();
|
596
sources/js/models.js
Normal file
|
@ -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]);
|
||||
};
|
||||
}]);
|
||||
})();
|
210
sources/js/notifications.js
Normal file
|
@ -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 = '<audio autoplay="autoplay"><source src="' + audioFile + '.ogg" type="audio/ogg" /><source src="' + audioFile + '.mp3" type="audio/mpeg" /></audio>';
|
||||
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
|
||||
};
|
||||
}]);
|
86
sources/js/plugin-directive.js
Normal file
|
@ -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);
|
||||
}
|
||||
}]
|
||||
};
|
||||
}]);
|
||||
})();
|
530
sources/js/plugins.js
Normal file
|
@ -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('<iframe></iframe>')
|
||||
.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('<iframe></iframe>')
|
||||
.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('<iframe></iframe>')
|
||||
.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('<iframe></iframe>')
|
||||
.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('<a></a>')
|
||||
.attr('target', '_blank')
|
||||
.attr('href', url)
|
||||
.append(angular.element('<img>')
|
||||
.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('<audio controls></audio>')
|
||||
.addClass('embed')
|
||||
.attr('width', '560')
|
||||
.append(angular.element('<source></source>')
|
||||
.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('<video autoplay loop muted></video>')
|
||||
.addClass('embed')
|
||||
.attr('width', '560')
|
||||
.append(angular.element('<source></source>')
|
||||
.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('<iframe></iframe>')
|
||||
.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('<iframe></iframe>')
|
||||
.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('<iframe></iframe>')
|
||||
.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('<img>')
|
||||
.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 = '<link rel="stylesheet" href="' + data.stylesheet + '"></link>';
|
||||
document.getElementsByTagName('head')[0].innerHTML += stylesheet;
|
||||
}
|
||||
element.innerHTML = '<div style="clear:both">' + data.div + '</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('<a></a>')
|
||||
.attr('target', '_blank')
|
||||
.attr('href', url)
|
||||
.append(angular.element('<img>')
|
||||
.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("<script ");
|
||||
var content = data.html.substr(0, scriptIndex);
|
||||
// Set DNT (Do Not Track)
|
||||
content = content.replace("<blockquote class=\"twitter-tweet\">", "<blockquote class=\"twitter-tweet\" data-dnt=\"true\">");
|
||||
element.innerHTML = content;
|
||||
|
||||
// The script tag needs to be generated manually or the browser won't load it
|
||||
var scriptElem = document.createElement('script');
|
||||
// Hardcoding the URL here, I don't suppose it's going to change anytime soon
|
||||
scriptElem.src = "//platform.twitter.com/widgets.js";
|
||||
element.appendChild(scriptElem);
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Vine plugin
|
||||
*/
|
||||
var vinePlugin = new UrlPlugin('Vine', function (url) {
|
||||
var regexp = /^https?:\/\/(www\.)?vine.co\/v\/([a-zA-Z0-9]+)(\/.*)?/i,
|
||||
match = url.match(regexp);
|
||||
if (match) {
|
||||
var id = match[2], embedurl = "https://vine.co/v/" + id + "/embed/simple?audio=1";
|
||||
var element = angular.element('<iframe></iframe>')
|
||||
.addClass('vine-embed')
|
||||
.attr('src', embedurl)
|
||||
.attr('width', '600')
|
||||
.attr('height', '600')
|
||||
.attr('frameborder', '0');
|
||||
return element.prop('outerHTML') + '<script async src="//platform.vine.co/static/scripts/embed.js" charset="utf-8"></script>';
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, audioPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, giphyPlugin, tweetPlugin, vinePlugin]
|
||||
};
|
||||
|
||||
|
||||
});
|
||||
})();
|
82
sources/js/settings.js
Normal file
|
@ -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;
|
||||
}]);
|
||||
|
||||
})();
|
29
sources/js/utils.js
Normal file
|
@ -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
|
||||
};
|
||||
});
|
150
sources/js/websockets.js
Normal file
|
@ -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
|
||||
};
|
||||
|
||||
}]);
|
||||
})();
|
1284
sources/js/weechat.js
Normal file
21
sources/js/whenscrolled-directive.js
Normal file
|
@ -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)();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
25
sources/manifest.json
Normal file
|
@ -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"
|
||||
]
|
||||
}
|
29
sources/manifest.webapp
Normal file
|
@ -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"
|
||||
}
|
36
sources/package.json
Normal file
|
@ -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');\""
|
||||
}
|
||||
}
|
2
sources/run_tests.sh
Executable file
|
@ -0,0 +1,2 @@
|
|||
./node_modules/.bin/jshint js/*.js test/unit/*.js
|
||||
npm test
|
46
sources/serviceworker.js
Normal file
|
@ -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/');
|
||||
}
|
||||
*/
|
||||
}));
|
||||
};
|
26
sources/test/e2e/scenarios.js
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
49
sources/test/karma.conf.js
Normal file
|
@ -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'
|
||||
}
|
||||
|
||||
});
|
||||
};
|
19
sources/test/protractor-conf.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
|
||||
specs: [
|
||||
'e2e/*.js'
|
||||
],
|
||||
|
||||
capabilities: {
|
||||
'browserName': 'firefox'
|
||||
},
|
||||
|
||||
baseUrl: 'http://localhost:8000/',
|
||||
|
||||
framework: 'jasmine',
|
||||
|
||||
jasmineNodeOpts: {
|
||||
defaultTimeoutInterval: 30000
|
||||
}
|
||||
};
|
95
sources/test/unit/filters.js
Normal file
|
@ -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('<a href="#" onclick="openBuffer(\'#foo\');">#foo</a>');
|
||||
}));
|
||||
|
||||
it('should not mess up IRC channels surrounded by HTML entities', inject(function(irclinkyFilter) {
|
||||
expect(irclinkyFilter('<"#foo">')).toEqual('<"<a href="#" onclick="openBuffer(\'#foo">\');">#foo"></a>');
|
||||
}));
|
||||
|
||||
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 <div class="colourbox" style="background-color:#123456"></div>');
|
||||
expect(inlinecolourFilter('#aabbcc')).toEqual('#aabbcc <div class="colourbox" style="background-color:#aabbcc"></div>');
|
||||
}));
|
||||
|
||||
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) <div class="colourbox" style="background-color:rgb(1,2,3)"></div>');
|
||||
expect(inlinecolourFilter('rgb(1,2,3);')).toEqual('rgb(1,2,3); <div class="colourbox" style="background-color:rgb(1,2,3);"></div>');
|
||||
expect(inlinecolourFilter('rgba(1,2,3,0.4)')).toEqual('rgba(1,2,3,0.4) <div class="colourbox" style="background-color:rgba(1,2,3,0.4)"></div>');
|
||||
expect(inlinecolourFilter('rgba(255,123,0,0.5);')).toEqual('rgba(255,123,0,0.5); <div class="colourbox" style="background-color:rgba(255,123,0,0.5);"></div>');
|
||||
}));
|
||||
|
||||
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 ) <div class="colourbox" style="background-color:rgb( 1\t, 2 , 3 )"></div>');
|
||||
}));
|
||||
|
||||
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) <div class="colourbox" style="background-color:rgb(1,2,3)"></div> #123456 <div class="colourbox" style="background-color:#123456"></div>');
|
||||
expect(inlinecolourFilter('#f00baa #123456 #234567')).toEqual('#f00baa <div class="colourbox" style="background-color:#f00baa"></div> #123456 <div class="colourbox" style="background-color:#123456"></div> #234567 <div class="colourbox" style="background-color:#234567"></div>');
|
||||
expect(inlinecolourFilter('rgba(1,2,3,0.4) foorgb(50,100,150)')).toEqual('rgba(1,2,3,0.4) <div class="colourbox" style="background-color:rgba(1,2,3,0.4)"></div> foorgb(50,100,150) <div class="colourbox" style="background-color:rgb(50,100,150)"></div>');
|
||||
}));
|
||||
|
||||
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<p>b<i>c<b>d</b>e<b>f</b>g</i>h</p>i',
|
||||
expected = '<span>A</span><p><span>B</span><i><span>C</span><b><span>D</span></b><span>E</span><b><span>F</span></b><span>G</span></i><span>H</span></p><span>I</span>',
|
||||
result = DOMfilterFilter(dom, 'uppercase').$$unwrapTrustedValue();
|
||||
expect(result).toEqual(expected);
|
||||
}));
|
||||
|
||||
it('should pass additional arguments to the filter', inject(function(DOMfilterFilter) {
|
||||
var dom = '1<p>2</p>3.14159265',
|
||||
expected = '<span>1.00</span><p><span>2.00</span></p><span>3.14</span>',
|
||||
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.
|
||||
}));
|
||||
});
|
||||
});
|
169
sources/test/unit/plugins.js
Normal file
|
@ -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);
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
33
sources/webapp.manifest.json
Normal file
|
@ -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"
|
||||
}]
|
||||
}
|