parent
1092f4541a
commit
5c8fb6e975
@ -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:
|
||||
|
||||
{project} Copyright (C) {year} {fullname}
|
||||
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>.
|
@ -0,0 +1,11 @@
|
||||
# Snapcast Control
|
||||
|
||||
A GTK Controller for the Snapcast multiroom sound system written in JavaScript for GJS.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
$ meson build
|
||||
$ cd build
|
||||
$ sudo ninja install
|
||||
```
|
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface domain="org-snapcast-control-gtk">
|
||||
<!-- interface-requires gtk+ 3.6 -->
|
||||
<menu id="app-menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Servers</attribute>
|
||||
<attribute name="action">app.servers</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_About Snap Control</attribute>
|
||||
<attribute name="action">app.about</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Quit</attribute>
|
||||
<attribute name="action">app.quit</attribute>
|
||||
<attribute name="accel"><Primary>q</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
</interface>
|
@ -0,0 +1,2 @@
|
||||
.volume-label {
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkListStore" id="storeServerProps">
|
||||
<columns>
|
||||
<!-- column-name Label -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name Value -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkListStore" id="storeServers">
|
||||
<columns>
|
||||
<!-- column-name Name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name Hostname -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name Domain -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name IP -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name Port -->
|
||||
<column type="guint64"/>
|
||||
<!-- column-name UserDefined -->
|
||||
<column type="gboolean"/>
|
||||
</columns>
|
||||
</object>
|
||||
<template class="Gjs_SnapControlWindow" parent="GtkApplicationWindow">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_menubar">False</property>
|
||||
<child type="titlebar">
|
||||
<object class="GtkHeaderBar" id="titlebar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Snapcast Control</property>
|
||||
<property name="subtitle" translatable="yes">Disconnected</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<property name="decoration_layout">:menu,minimize,maximize,close</property>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="cbtServers">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">storeServers</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="Hostname"/>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="Port"/>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="spinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="btnInfo">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">dialog-information-symbolic</property>
|
||||
<property name="use_fallback">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="btnAppMenu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">open-menu-symbolic</property>
|
||||
<property name="use_fallback">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="sw">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_width">600</property>
|
||||
<property name="min_content_height">250</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="vBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="labelState">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes"><i>Not connected to Snapcast server.</i></property>
|
||||
<property name="use_markup">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
<object class="GtkPopover" id="popoverConfig">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="border_width">6</property>
|
||||
<property name="relative_to">btnInfo</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="poTreeView">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">storeServerProps</property>
|
||||
<property name="headers_visible">False</property>
|
||||
<property name="enable_search">False</property>
|
||||
<property name="show_expanders">False</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection">
|
||||
<property name="mode">none</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="Label">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">Label</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="0"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="Value">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">Value</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="1"/>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
@ -0,0 +1,184 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkImage" id="imageMenuItemDetails">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">dialog-information-symbolic</property>
|
||||
<property name="use_fallback">True</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="imageMenuItemRemove">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-delete-symbolic</property>
|
||||
<property name="use_fallback">True</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="menuDetails">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="menuItemDetails">
|
||||
<property name="label" translatable="yes">Details</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Show details and options for this client.</property>
|
||||
<property name="image">imageMenuItemDetails</property>
|
||||
<property name="use_stock">False</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="menuItemRemove">
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Remove this client from snapcast server (it will be unavailable until it reconnects)</property>
|
||||
<property name="image">imageMenuItemRemove</property>
|
||||
<property name="use_stock">False</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkAdjustment" id="volumeAdjustment">
|
||||
<property name="upper">100</property>
|
||||
<property name="step_increment">1</property>
|
||||
<property name="page_increment">5</property>
|
||||
</object>
|
||||
<template class="Gjs_ClientWidget" parent="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">False</property>
|
||||
<property name="vexpand">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="iconStatus">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-no</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="labelName">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Client Name</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="single_line_mode">True</property>
|
||||
<property name="track_visited_links">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="btnConfig">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="popup">menuDetails</property>
|
||||
<property name="use_popover">False</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="btnMute">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">audio-volume-muted-symbolic</property>
|
||||
<property name="use_fallback">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkScale" id="scaleVolume">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="adjustment">volumeAdjustment</property>
|
||||
<property name="show_fill_level">True</property>
|
||||
<property name="fill_level">100</property>
|
||||
<property name="round_digits">0</property>
|
||||
<property name="draw_value">False</property>
|
||||
<property name="value_pos">bottom</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="labelVolume">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">100%</property>
|
||||
<property name="justify">right</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="single_line_mode">True</property>
|
||||
<property name="max_width_chars">5</property>
|
||||
<property name="track_visited_links">False</property>
|
||||
<style>
|
||||
<class name="volume-label"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<template class="Gjs_GroupOptions" parent="GtkWindow">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Streams</property>
|
||||
<property name="role">group-options</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="deletable">False</property>
|
||||
<property name="startup_id">group-options</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">15</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="min_content_width">250</property>
|
||||
<property name="min_content_height">150</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="shadow_type">none</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="vBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="bBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnCancel">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnSave">
|
||||
<property name="label">gtk-save</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkAdjustment" id="adjVolume">
|
||||
<property name="upper">100</property>
|
||||
<property name="step_increment">5</property>
|
||||
<property name="page_increment">10</property>
|
||||
</object>
|
||||
<template class="Gjs_GroupWidget" parent="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">False</property>
|
||||
<property name="vexpand">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="hBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="iconState">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-media-pause</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="btnComboStream">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="labelName">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScale" id="scaleVolume">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="adjustment">adjVolume</property>
|
||||
<property name="round_digits">1</property>
|
||||
<property name="digits">0</property>
|
||||
<property name="value_pos">bottom</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnConfig">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">applications-system-symbolic</property>
|
||||
<property name="use_fallback">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="btnMute">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">audio-volume-muted-symbolic</property>
|
||||
<property name="use_fallback">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
Binary file not shown.
@ -0,0 +1,56 @@
|
||||
data_resource = gnome.compile_resources(app_id + '.data',
|
||||
app_id + '.data.gresource.xml',
|
||||
source_dir: '.',
|
||||
gresource_bundle: true,
|
||||
install: true,
|
||||
install_dir : pkgdatadir
|
||||
)
|
||||
|
||||
appsdir = join_paths(get_option('datadir'), 'applications')
|
||||
desktop = intl.merge_file(
|
||||
input : app_id + '.desktop.in',
|
||||
output : app_id + '.desktop',
|
||||
po_dir : '../po',
|
||||
type : 'desktop',
|
||||
install: true,
|
||||
install_dir: appsdir
|
||||
)
|
||||
|
||||
gsettingsdir = join_paths(get_option('datadir'), 'glib-2.0', 'schemas')
|
||||
install_data(app_id + '.gschema.xml', install_dir : gsettingsdir)
|
||||
|
||||
appdatadir = join_paths(get_option('datadir'), 'appdata')
|
||||
appdata = intl.merge_file(
|
||||
input : app_id + '.appdata.xml.in',
|
||||
output : app_id + '.appdata.xml',
|
||||
po_dir : '../po',
|
||||
type : 'xml',
|
||||
install: true,
|
||||
install_dir: appdatadir
|
||||
)
|
||||
|
||||
install_data(
|
||||
app_id + '.png',
|
||||
install_dir : join_paths(get_option('datadir'), 'icons', 'hicolor', '512x512', 'apps')
|
||||
)
|
||||
|
||||
|
||||
install_data(
|
||||
app_id + '.svg',
|
||||
install_dir : join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps')
|
||||
)
|
||||
|
||||
servicedir = join_paths(get_option('datadir'), 'dbus-1', 'services')
|
||||
|
||||
service_conf = configuration_data()
|
||||
service_conf.set('APP_ID', app_id)
|
||||
service_conf.set('DATA_DIR', pkgdatadir)
|
||||
service_conf.set('BIN_DIR', join_paths(get_option('prefix'), get_option('datadir'), app_id))
|
||||
|
||||
configure_file(
|
||||
input: '@0@.service.in'.format(app_id),
|
||||
output: '@0@.service'.format(app_id),
|
||||
configuration: service_conf,
|
||||
install: true,
|
||||
install_dir: servicedir
|
||||
)
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop">
|
||||
<id>org.snapcast.control.gtk</id>
|
||||
<licence>GPL-3.0</licence>
|
||||
<description>
|
||||
<_p>
|
||||
A GTK Controller for Snapcast multiroom audio sound system.
|
||||
</_p>
|
||||
</description>
|
||||
<url type="homepage">https://github.com</url>
|
||||
<updatecontact>user@no.reply.github.com</updatecontact>
|
||||
</component>
|
@ -0,0 +1,7 @@
|
||||
[Unit]
|
||||
Description=org.snapcast.control.gtk
|
||||
#Documentation=http://...
|
||||
|
||||
[BusName]
|
||||
Name=org.snapcast.control.gtk
|
||||
#Service=org.snapcast.control.gtk.service
|
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/snapcast/control/gtk">
|
||||
<file preprocess="xml-stripblanks">app-menu.ui</file>
|
||||
<file preprocess="xml-stripblanks">appwindow.ui</file>
|
||||
<file preprocess="xml-stripblanks">group.ui</file>
|
||||
<file preprocess="xml-stripblanks">group-options.ui</file>
|
||||
<file preprocess="xml-stripblanks">client.ui</file>
|
||||
<file>application.css</file>
|
||||
<file>org.snapcast.control.gtk.png</file>
|
||||
<file>snapcontrol.png</file>
|
||||
</gresource>
|
||||
</gresources>
|
@ -0,0 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Icon=org.snapcast.control.gtk
|
||||
Type=Application
|
||||
Name=Snapcast Control
|
||||
Comment=A Snapcast Controller for GNOME
|
||||
#Exec=/usr/local/bin/snapcontrol
|
||||
Exec=gapplication launch org.snapcast.control.gtk
|
||||
DBusActivatable=true
|
||||
StartupNotify=true
|
||||
Categories=AudioVideo;Audio;Mixer;GNOME;GTK;Utility;
|
||||
Keywords=Snapcast;Volume;Audio;Settings;Advanced;
|
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<schemalist gettext-domain="org.snapcast.control.gtk">
|
||||
<schema id="org.snapcast.control.gtk" path="/org/snapcast/control/gtk/">
|
||||
<key name="servers" type="aa{sv}">
|
||||
<default>[]</default>
|
||||
<summary>Snapcast Servers</summary>
|
||||
<description>A list of manually entered snapcast servers.</description>
|
||||
</key>
|
||||
<key name="last-used" type="s">
|
||||
<default>"127.0.0.1:1705"</default>
|
||||
<summary>Last used server</summary>
|
||||
<description>The ip address and port of the last used server.</description>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
After Width: | Height: | Size: 71 KiB |
@ -0,0 +1,3 @@
|
||||
[D-BUS Service]
|
||||
Name=@APP_ID@
|
||||
Exec=@BIN_DIR@/@APP_ID@ --gapplication-service
|
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 80 KiB |
@ -0,0 +1,46 @@
|
||||
project(
|
||||
'snapcontrol', 'c',
|
||||
version: '1.0.0',
|
||||
license: 'BSD3',
|
||||
meson_version: '>= 0.50.0',
|
||||
)
|
||||
|
||||
app_command = 'snapcontrol'
|
||||
app_id = 'org.snapcast.control.gtk'
|
||||
|
||||
gnome = import('gnome')
|
||||
intl = import('i18n')
|
||||
|
||||
config_h = configuration_data()
|
||||
GETTEXT_PACKAGE = app_id
|
||||
config_h.set_quoted('GETTEXT_PACKAGE', GETTEXT_PACKAGE)
|
||||
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir')))
|
||||
configure_file(
|
||||
output: 'config.h',
|
||||
configuration: config_h,
|
||||
)
|
||||
add_global_arguments([
|
||||
'-DHAVE_CONFIG_H',
|
||||
'-I' + meson.build_root(),
|
||||
], language: 'c')
|
||||
|
||||
app_configuration = configuration_data()
|
||||
|
||||
app_configuration.set('GJS', find_program('gjs').path())
|
||||
app_configuration.set('PACKAGE_NAME', app_id)
|
||||
app_configuration.set('PACKAGE_VERSION', meson.project_version())
|
||||
app_configuration.set('prefix', get_option('prefix'))
|
||||
|
||||
#pkgdatadir = join_paths(get_option('datadir'), app_command, app_id)
|
||||
pkgdatadir = join_paths(get_option('datadir'), app_id)
|
||||
libexecdir = join_paths(get_option('prefix'), get_option('libexecdir'))
|
||||
app_configuration.set('libdir', join_paths(get_option('prefix'), get_option('libdir')))
|
||||
app_configuration.set('pkgdatadir', pkgdatadir)
|
||||
app_configuration.set('libexec', libexecdir)
|
||||
|
||||
#install_data('helper-bin', install_dir: libexecdir)
|
||||
|
||||
subdir('src')
|
||||
subdir('data')
|
||||
subdir('po')
|
||||
meson.add_install_script('meson/meson_post_install.py')
|
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
|
||||
prefix = pathlib.Path(os.environ.get('MESON_INSTALL_PREFIX', '/usr/local'))
|
||||
datadir = prefix / 'share'
|
||||
bindir = prefix / 'bin'
|
||||
destdir = os.environ.get('DESTDIR', '')
|
||||
|
||||
src = os.path.join(datadir, 'org.snapcast.control.gtk', 'org.snapcast.control.gtk')
|
||||
dest = os.path.join(bindir, 'snapcontrol')
|
||||
subprocess.call(['ln', '-s', '-f', src, dest])
|
||||
|
||||
if not destdir:
|
||||
print('Compiling gsettings schemas...')
|
||||
subprocess.call(['glib-compile-schemas', os.path.join(datadir, 'glib-2.0/schemas')])
|
||||
|
||||
print('Updating icon cache...')
|
||||
subprocess.call(['gtk-update-icon-cache', '-qtf', str(datadir / 'icons' / 'hicolor' / 'scalable')])
|
||||
|
||||
print('Updating desktop database...')
|
||||
subprocess.call(['update-desktop-database', '-q', str(datadir / 'applications')])
|
@ -0,0 +1,45 @@
|
||||
{
|
||||
"app-id" : "org.snapcast.control.gtk",
|
||||
"runtime" : "org.gnome.Platform",
|
||||
"runtime-version" : "3.28",
|
||||
"branch" : "stable",
|
||||
"sdk" : "org.gnome.Sdk",
|
||||
"command" : "snapcontrol",
|
||||
"finish-args" : [
|
||||
"--share=ipc",
|
||||
"--socket=x11",
|
||||
"--socket=wayland",
|
||||
"--share=network",
|
||||
"--filesystem=xdg-run/dconf",
|
||||
"--filesystem=~/.config/dconf:ro",
|
||||
"--talk-name=ca.desrt.dconf",
|
||||
"--env=DCONF_USER_CONFIG_DIR=.config/dconf"
|
||||
],
|
||||
"cleanup" : [
|
||||
"/include",
|
||||
"/lib/pkgconfig",
|
||||
"/share/pkgconfig",
|
||||
"/share/aclocal",
|
||||
"/man",
|
||||
"/share/man",
|
||||
"/share/gtk-doc",
|
||||
"/share/vala",
|
||||
"*.la",
|
||||
"*.a"
|
||||
],
|
||||
"modules" : [
|
||||
{
|
||||
"name" : "SnapcastControl",
|
||||
"sources" : [
|
||||
{
|
||||
"type" : "git",
|
||||
"url" : "https://github.com/"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"build-options" : {
|
||||
"env" : {
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
# List of source files containing translatable strings.
|
||||
# Please keep this file sorted alphabetically.
|
@ -0,0 +1,2 @@
|
||||
# List of source files that should *not* be translated.
|
||||
# Please keep this file sorted alphabetically.
|
@ -0,0 +1,14 @@
|
||||
langs = [
|
||||
# 'it'
|
||||
]
|
||||
|
||||
if langs.length() > 0
|
||||
intl.gettext(GETTEXT_PACKAGE,
|
||||
languages: langs,
|
||||
args: [
|
||||
'--from-code=UTF-8',
|
||||
'--keyword=g_dngettext:2,3',
|
||||
'--add-comments',
|
||||
],
|
||||
)
|
||||
endif
|
@ -0,0 +1,329 @@
|
||||
'use strict';
|
||||
|
||||
const {Gdk, Gio, GLib, Gtk, GObject} = imports.gi;
|
||||
|
||||
const Util = imports.util;
|
||||
const Widgets = imports.widgets;
|
||||
const LibSnapcast = imports.snapcast;
|
||||
const Avahi = imports.avahi;
|
||||
|
||||
var SnapControlWindow = GObject.registerClass({
|
||||
Template: Util.getBuilderFile('appwindow.ui')
|
||||
, InternalChildren: [
|
||||
'storeServerProps'
|
||||
, 'storeServers'
|
||||
, 'titlebar'
|
||||
, 'cbtServers'
|
||||
, 'spinner'
|
||||
, 'btnInfo'
|
||||
, 'btnAppMenu'
|
||||
, 'vBox'
|
||||
, 'labelState'
|
||||
, 'popoverConfig'
|
||||
]
|
||||
}, class SnapControlWindow extends Gtk.ApplicationWindow {
|
||||
_init(params) {
|
||||
super._init(Object.assign({
|
||||
title: _("Snapcast Control")
|
||||
, role: _("Snapcast Control")
|
||||
, 'show-menubar': false
|
||||
, 'icon-name': pkg.id
|
||||
, name: 'snap-control-window'
|
||||
}, params));
|
||||
|
||||
this._settings = Util.getSettings(pkg.id);
|
||||
this._btnAppMenu.set_menu_model(this.application.get_app_menu());
|
||||
|
||||
this.AS = new Avahi.AvahiSnapcast();
|
||||
this.AS.connect('added', (AS, server) => {
|
||||
// XXX: Check if not exists
|
||||
this._storeServers.set(
|
||||
this._storeServers.append()
|
||||
, [0, 1, 2, 3, 4, 5]
|
||||
, [server.name, server.hostname.toLowerCase(), server.domain, server.ip, server.port, false]
|
||||
);
|
||||
this._cbtServers.set_sensitive(true);
|
||||
if (this.snapcast._state == LibSnapcast.ClientStates.connected) {
|
||||
let [tie, iter] = this._storeServers.get_iter_first();
|
||||
let model = this._cbtServers.get_model();
|
||||
while (tie) {
|
||||
let host = [this._storeServers.get_value(iter, 3), this._storeServers.get_value(iter, 4)].join(':');
|
||||
if (host === this.snapcast.host) {
|
||||
this._cbtServers.set_active_iter(iter);
|
||||
break;
|
||||
}
|
||||
tie = this._storeServers.iter_next(iter);
|
||||
}
|
||||
} else if (this._cbtServers.get_active() === -1) {
|
||||
this._cbtServers.set_active(0);
|
||||
}
|
||||
});
|
||||
|
||||
this.AS.connect('removed', (AS, name) => {
|
||||
let [tie, iter] = this._storeServers.get_iter_first();
|
||||
let model = this._cbtServers.get_model();
|
||||
while (tie) {
|
||||
if (this._storeServers.get_value(iter, 0) === name) {
|
||||
if (this._storeServers.get_value(iter, 5) === false) {
|
||||
this._storeServers.remove(iter);
|
||||
}
|
||||
break;
|
||||
}
|
||||
tie = this._storeServers.iter_next(iter);
|
||||
}
|
||||
if (this._cbtServers.get_active() === -1) {
|
||||
this._cbtServers.set_active(0);
|
||||
}
|
||||
});
|
||||
|
||||
this._spinner.start();
|
||||
|
||||
this._popoverConfig.connect('closed', () => {
|
||||
this._btnInfo.set_active(false);
|
||||
});
|
||||
this._btnInfo.connect('toggled', (btn) => {
|
||||
if (btn.get_active()) {
|
||||
this._popoverConfig.set_relative_to(this._btnInfo);
|
||||
//this._popoverConfig.show_all();
|
||||
this._popoverConfig.popup();
|
||||
} else {
|
||||
this._popoverConfig.popdown();
|
||||
}
|
||||
});
|
||||
/*
|
||||
this._cbtServers.set_has_tooltip(true);
|
||||
this._cbtServers.connect('query-tooltip', (widget, x, y, keyboar_mode, tooltip) => {
|
||||
this._popoverConfig.set_relative_to(widget);
|
||||
this._popoverConfig.show_all();
|
||||
this._popoverConfig.popup();
|
||||
log(this._popoverConfig);
|
||||
return false;
|
||||
});
|
||||
*/
|
||||
this._btnAppMenu = new Gtk.MenuButton();
|
||||
this._btnAppMenu.set_menu_model(this.application.get_app_menu());
|
||||
this._btnAppMenu.set_image(new Gtk.Image({ 'icon-name': 'open-menu-symbolic' }));
|
||||
|
||||
this.show_all();
|
||||
|
||||
let lastUsed = this._settings.get_string('last-used');
|
||||
this.snapcast = new LibSnapcast.Snapcast(lastUsed);
|
||||
this.snapcast.connect("state::ready", this._on_snapcast_ready.bind(this));
|
||||
this.snapcast.connect("state::disconnected", this._on_snapcast_disconnected.bind(this));
|
||||
this.snapcast.connect("state::connecting", this._on_snapcast_connecting.bind(this));
|
||||
this.snapcast.connect("updated", this._on_snapcast_updated.bind(this));
|
||||
this.snapcast.enable();
|
||||
|
||||
this._cbtServers.connect('changed', (combo) => {
|
||||
let [tie, tree_iter] = combo.get_active_iter();
|
||||
let activeItem = combo.get_active();
|
||||
let model = combo.get_model();
|
||||
if (tie) {
|
||||
let hostname = this._storeServers.get_value(tree_iter, 1)
|
||||
, domain = this._storeServers.get_value(tree_iter, 2)
|
||||
, ip = this._storeServers.get_value(tree_iter, 3)
|
||||
, port = this._storeServers.get_value(tree_iter, 4)
|
||||
, host;
|
||||
if (ip !== "") {
|
||||
host = ip;
|
||||
} else {
|
||||
host = hostname;
|
||||
if (domain !== "") {
|
||||
host += '.' + domain;
|
||||
}
|
||||
}
|
||||
if (port === "") {
|
||||
port = 1705;
|
||||
}
|
||||
this.snapcast.host = [host, port].join(':');
|
||||
}
|
||||
});
|
||||
|
||||
this.AS.enable();
|
||||
}
|
||||
|
||||
_on_snapcast_updated() {
|
||||
log("SNAPCAST_UPDATED");
|
||||
this.updateInfoPopover();
|
||||
//this._popoverConfig.add(this.buildInfoPopover());
|
||||
}
|
||||
|
||||
_on_snapcast_disconnected() {
|
||||
log("SNAPCAST_DISCONNECTED");
|
||||
this._titlebar.set_subtitle(_("Disconnected"));
|
||||
this._vBox.foreach((child) => {
|
||||
if (child instanceof Widgets.GroupWidget) {
|
||||
this._vBox.remove(child);
|
||||
}
|
||||
});
|
||||
if (this._popoverConfig.is_visible()) {
|
||||
this._popoverConfig.popdown();
|
||||
}
|
||||
this._btnInfo.set_sensitive(false);
|
||||
this._btnInfo.set_visible(false);
|
||||
this._labelState.set_visible(true);
|
||||
}
|
||||
|
||||
_on_snapcast_connecting() {
|
||||
this._spinner.start();
|
||||
this._spinner.set_visible(true);
|
||||
}
|
||||
|
||||
_on_snapcast_ready() {
|
||||
log("SNAPCAST_READY");
|
||||
this._titlebar.set_subtitle(_("Connected"));
|
||||
this._settings.set_string('last-used', this.snapcast.host);
|
||||
this._labelState.set_visible(false);
|
||||
this._spinner.stop();
|
||||
this._spinner.set_visible(false);
|
||||
this._btnInfo.set_sensitive(true);
|
||||
this._btnInfo.set_visible(true);
|
||||
this.snapcast.sServer.connect("streams::added", this._on_snapcast_stream_added.bind(this));
|
||||
this.snapcast.sServer.connect("streams::deleted", this._on_snapcast_stream_deleted.bind(this));
|
||||
this.snapcast.sServer.connect("groups::added", this._on_snapcast_group_added.bind(this));
|
||||
this.snapcast.sServer.connect("groups::deleted", this._on_snapcast_group_deleted.bind(this));
|
||||
this.snapcast.sServer.connect("clients::added", this._on_snapcast_client_added.bind(this));
|
||||
this.snapcast.sServer.connect("clients::deleted", this._on_snapcast_client_deleted.bind(this));
|
||||
}
|
||||
|
||||
_on_snapcast_stream_added() {
|
||||
// XXX
|
||||
}
|
||||
|
||||
_on_snapcast_stream_deleted() {
|
||||
// XXX
|
||||
}
|
||||
|
||||
_on_snapcast_group_added(event, group) {
|
||||
this._vBox.add(new Widgets.GroupWidget(this, this.snapcast, group));
|
||||
this._refresh_window_size();
|
||||
}
|
||||
|
||||
_on_snapcast_group_deleted(event, group_id) {
|
||||
this._refresh_window_size();
|
||||
}
|
||||
|
||||
_on_snapcast_client_added(event, client) {
|
||||
this._refresh_window_size();
|
||||
}
|
||||
|
||||
_on_snapcast_client_deleted(event, client) {
|
||||
this._refresh_window_size();
|
||||
}
|
||||
|
||||
_refresh_window_size() {
|
||||
let [wm, wn] = this.get_preferred_width();
|
||||
let [hm, hn] = this._vBox.get_preferred_height();
|
||||
let [thm, thn] = this._titlebar.get_preferred_height();
|
||||
let ch = 0;
|
||||
this._vBox.get_children().forEach((child) => {
|
||||
let [chm, chn] = child.get_preferred_height();
|
||||
ch = ch + chn + (this._vBox.get_spacing() * 2);
|
||||
});
|
||||
ch += this._vBox.get_parent().get_margin_top() + this._vBox.get_parent().get_margin_bottom();
|
||||
let nh = thn + this._vBox.get_margin_top() + this._vBox.get_margin_bottom() + ch;
|
||||
let display = this.get_display();
|
||||
let monitor = display.get_monitor_at_window(this.get_window());
|
||||
let wa = monitor.get_workarea();
|
||||
if (nh > wa.height - thn) {
|
||||
nh = wa.height - thn;
|
||||
}
|
||||
this.resize(wn, nh);
|
||||
}
|
||||
|
||||
updateInfoPopover() {
|
||||
this._storeServerProps.clear();
|
||||
let props = this.snapcast.sServer.server;
|
||||
this._storeServerProps.set(this._storeServerProps.append(), [0,1],
|
||||
[_("Name: "), props.snapserver.name]);
|
||||
this._storeServerProps.set(this._storeServerProps.append(), [0,1],
|
||||
[_("Hostname: "), props.host.name]);
|
||||
this._storeServerProps.set(this._storeServerProps.append(), [0,1],
|
||||
[_("Operating System: "), props.host.os]);
|
||||
if (props.host.arch !== "") {
|
||||
this._storeServerProps.set(this._storeServerProps.append(), [0,1],
|
||||
[_("Architecture: "), props.host.arch]);
|
||||
}
|
||||
this._storeServerProps.set(this._storeServerProps.append(), [0,1],
|
||||
[_("Snapserver Version: "), props.snapserver.version]);
|
||||
this._storeServerProps.set(this._storeServerProps.append(), [0,1],
|
||||
[_("Protocol Version: "), props.snapserver.protocolVersion]);
|
||||
this._storeServerProps.set(this._storeServerProps.append(), [0,1],
|
||||
[_("Control Version: "), props.snapserver.controlProtocolVersion]);
|
||||
this._storeServerProps.set(this._storeServerProps.append(), [0,1],
|
||||
[_("Streams: "), Object.keys(this.snapcast.sServer.streams).length]);
|
||||
}
|
||||
|
||||
showServersWindow() {
|
||||
log('SHOW SERVER WINDOWS');
|
||||
let modal = new Gtk.Dialog({
|
||||
default_width: 250
|
||||
, modal: true
|
||||
, transient_for: this
|
||||
, title: _("Servers")
|
||||
, use_header_bar: false
|
||||
});
|
||||
|
||||
let contentArea = modal.get_content_area()
|
||||
, actionArea = modal.get_action_area();
|
||||
|
||||
let treeview = new Gtk.TreeView({
|
||||
model: this._storeServers
|
||||
, 'headers-visible': false
|
||||
});
|
||||
let sel = treeview.get_selection();
|
||||
sel.set_mode(Gtk.SelectionMode.NONE);
|
||||
|
||||
let renderer0 = new Gtk.CellRendererText();
|
||||
let renderer1 = new Gtk.CellRendererText();
|
||||
let renderer2 = new Gtk.CellRendererText();
|
||||
let renderer3 = new Gtk.CellRendererText();
|
||||
let renderer4 = new Gtk.CellRendererText();
|
||||
let renderer5 = new Gtk.CellRendererText();
|
||||
|
||||
let column0 = new Gtk.TreeViewColumn({ title: "ID" });
|
||||
let column1 = new Gtk.TreeViewColumn({ title: "Hostname" });
|
||||
let column2 = new Gtk.TreeViewColumn({ title: "Domain" });
|
||||
let column3 = new Gtk.TreeViewColumn({ title: "IP" });
|
||||
let column4 = new Gtk.TreeViewColumn({ title: "Port" });
|
||||
let column5 = new Gtk.TreeViewColumn({ title: "Manual" });
|
||||
|
||||
column0.pack_start(renderer0, false);
|
||||
column0.add_attribute(renderer0, "text", 0);
|
||||
column1.pack_start(renderer1, false);
|
||||
column1.add_attribute(renderer1, "text", 1);
|
||||
column2.pack_start(renderer2, false);
|
||||
column2.add_attribute(renderer2, "text", 2);
|
||||
column2.pack_start(renderer3, false);
|
||||
column2.add_attribute(renderer3, "text", 3);
|
||||
column2.pack_start(renderer4, false);
|
||||
column2.add_attribute(renderer4, "text", 4);
|
||||
column2.pack_start(renderer5, false);
|
||||
column2.add_attribute(renderer5, "text", 5);
|
||||
|
||||
treeview.append_column(column0);
|
||||
treeview.append_column(column1);
|
||||
treeview.append_column(column2);
|
||||
treeview.append_column(column3);
|
||||
treeview.append_column(column4);
|
||||
treeview.append_column(column5);
|
||||
|
||||
treeview.set_margin_top(10);
|
||||
treeview.set_margin_bottom(10);
|
||||
|
||||
contentArea.add(treeview);
|
||||
|
||||
let boxBtn = new Gtk.ButtonBox({
|
||||
orientation: Gtk.Orientation.HORIZONTAL
|
||||
});
|
||||
boxBtn.set_layout(Gtk.ButtonBoxStyle.EDGE);
|
||||
let btnOk = Gtk.Button.new_from_stock(Gtk.STOCK_OK);
|
||||
btnOk.connect("clicked", () => {
|
||||
modal.close();
|
||||
});
|
||||
boxBtn.add(btnOk);
|
||||
actionArea.add(boxBtn);
|
||||
|
||||
modal.show_all();
|
||||
}
|
||||
});
|
@ -0,0 +1,295 @@
|
||||
'use strict';
|
||||
|
||||
const pkg = imports.package;
|
||||
pkg.require({
|
||||
'Gdk': '3.0'
|
||||
, 'Gio': '2.0'
|
||||
, 'GLib': '2.0'
|
||||
, 'GObject': '2.0'
|
||||
, 'Gtk': '3.0'
|
||||
});
|
||||
const {Gdk, Gio, GLib, Gtk, GObject, Avahi, AvahiCore} = imports.gi;
|
||||
|
||||
const Signals = imports.signals;
|
||||
|
||||
// address.h
|
||||
const PROTO_UNSPEC = -1;
|
||||
|
||||
const IF_UNSPEC = -1;
|
||||
|
||||
// AvahiLookupFlags
|
||||
const LOOKUP_USE_WIDE_AREA = 1;
|
||||
const LOOKUP_USE_MULTICAST = 2;
|
||||
const LOOKUP_NO_TXT = 4;
|
||||
const LOOKUP_NO_ADDRESS = 8;
|
||||
|
||||
// AvahiLookupResultFlags
|
||||
const LOOKUP_RESULT_CACHED = 1;
|
||||
const LOOKUP_RESULT_WIDE_AREA = 2;
|
||||
const LOOKUP_RESULT_MULTICAST = 4;
|
||||
const LOOKUP_RESULT_LOCAL = 8;
|
||||
const LOOKUP_RESULT_OUR_OWN = 16;
|
||||
const LOOKUP_RESULT_STATIC = 32;
|
||||
const DNS_TYPE_A = 0x01;
|
||||
const DNS_TYPE_NS = 0x02;
|
||||
const DNS_TYPE_CNAME = 0x05;
|
||||
const DNS_TYPE_SOA = 0x06;
|
||||
const DNS_TYPE_PTR = 0x0C;
|
||||
const DNS_TYPE_HINFO = 0x0D;
|
||||
const DNS_TYPE_MX = 0x0F;
|
||||
const DNS_TYPE_TXT = 0x10;
|
||||
const DNS_TYPE_AAAA = 0x1C;
|
||||
const DNS_TYPE_SRV = 0x2;
|
||||
const DNS_CLASS_IN = 0x01;
|
||||
|
||||
//log(imports.gi.GIRepository.Repository.get_default().is_registered('AvahiCore', '0.6'));
|
||||
|
||||
const SNAPCAST_AVAHI_TCP = '_snapcast-tcp._tcp';
|
||||
const SNAPCAST_AVAHI_STREAM = '_snapcast-stream._tcp';
|
||||
const SNAPCAST_AVAHI_RCP = '_snapcast-rpc._tcp';
|
||||
|
||||
var AvahiSnapcast = class {
|
||||
constructor() {
|
||||
this.servers = {};
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.aclient = new Avahi.Client({flags: Avahi.ClientFlags.GA_CLIENT_FLAG_NO_FAIL});
|
||||
this.aclient.connect('state-changed', () => {
|
||||
log('AVAHI: state-changed')
|
||||
});
|
||||
this.aclient.start();
|
||||
this.abrowser = Avahi.ServiceBrowser.new_full(-1, AvahiCore.Protocol.INET, SNAPCAST_AVAHI_TCP, 'local', 0);
|
||||
this.abrowser.attach(this.aclient);
|
||||
this.abrowser.connect('all-for-now', () => {
|
||||
log('AVAHI: all-for-now')
|
||||
this.emit('all-for-now');
|
||||
});
|
||||
this.abrowser.connect('removed-service', (browser, if_index, protocol, name, service, domain, flags) => {
|
||||
this.removeServer(name);
|
||||
});
|
||||
this.abrowser.connect('cache-exhausted', () => {log('AVAHI: cache exhausted')});
|
||||
this.abrowser.connect('new-service', (browser, if_index, protocol, name, service, domain, flags) => {
|
||||
log('AVAHI: new-service')
|
||||
let aresolver = Avahi.ServiceResolver.new(if_index, protocol, name, service, domain, AvahiCore.Protocol.INET, 0);
|
||||
aresolver.attach(this.aclient);
|
||||
aresolver.connect('found', (resolver, if_index, protocol, name, service, domain, hostname, address, port, txt, flags, e) => {
|
||||
let ip = Gio.Resolver.get_default().lookup_by_name(hostname, null)[0].to_string();
|
||||
this.addServer(name, hostname, domain, ip, port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addServer(name, hostname, domain, ip, port) {
|
||||
if (name in this.servers) {
|
||||
return;
|
||||
}
|
||||
this.servers[name] = {
|
||||
name: name
|
||||
, hostname: hostname
|
||||
, domain: domain
|
||||
, ip: ip
|
||||
, port: port
|
||||
}
|
||||
log('AVAHI NEW: ' + JSON.stringify(this.servers[name]));
|
||||
this.emit('added', this.servers[name]);
|
||||
}
|
||||
|
||||
removeServer(name) {
|
||||
if (name in this.servers) {
|
||||
delete this.servers[name];
|
||||
log('AVAHI REMOVE: ' + name);
|
||||
this.emit('removed', name);
|
||||
}
|
||||
}
|
||||
|
||||
server(name) {
|
||||
return this.servers[name];
|
||||
}
|
||||
};
|
||||
Signals.addSignalMethods(AvahiSnapcast.prototype);
|
||||
|
||||
/*
|
||||
const avahiInterface = '<node>\
|
||||
<interface name="org.freedesktop.Avahi.Server">\
|
||||
<method name="GetVersionString">\
|
||||
<arg name="version" type="s" direction="out"/>\
|
||||
</method>\
|
||||
<method name="GetAPIVersion">\
|
||||
<arg name="version" type="u" direction="out"/>\
|
||||
</method>\
|
||||
<method name="GetHostName">\
|
||||
<arg name="name" type="s" direction="out"/>\
|
||||
</method>\
|
||||
<method name="SetHostName">\
|
||||
<arg name="name" type="s" direction="in"/>\
|
||||
</method>\
|
||||
<method name="GetHostNameFqdn">\
|
||||
<arg name="name" type="s" direction="out"/>\
|
||||
</method>\
|
||||
<method name="GetDomainName">\
|
||||
<arg name="name" type="s" direction="out"/>\
|
||||
</method>\
|
||||
<method name="IsNSSSupportAvailable">\
|
||||
<arg name="yes" type="b" direction="out"/>\
|
||||
</method>\
|
||||
<method name="GetState">\
|
||||
<arg name="state" type="i" direction="out"/>\
|
||||
</method>\
|
||||
<signal name="StateChanged">\
|
||||
<arg name="state" type="i"/>\
|
||||
<arg name="error" type="s"/>\
|
||||
</signal>\
|
||||
<method name="GetLocalServiceCookie">\
|
||||
<arg name="cookie" type="u" direction="out"/>\
|
||||
</method>\
|
||||
<method name="GetAlternativeHostName">\
|
||||
<arg name="name" type="s" direction="in"/>\
|
||||
<arg name="name" type="s" direction="out"/>\
|
||||
</method>\
|
||||
<method name="GetAlternativeServiceName">\
|
||||
<arg name="name" type="s" direction="in"/>\
|
||||
<arg name="name" type="s" direction="out"/>\
|
||||
</method>\
|
||||
<method name="GetNetworkInterfaceNameByIndex">\
|
||||
<arg name="index" type="i" direction="in"/>\
|
||||
<arg name="name" type="s" direction="out"/>\
|
||||
</method>\
|
||||
<method name="GetNetworkInterfaceIndexByName">\
|
||||
<arg name="name" type="s" direction="in"/>\
|
||||
<arg name="index" type="i" direction="out"/>\
|
||||
</method>\
|
||||
<method name="ResolveHostName">\
|
||||
<arg name="interface" type="i" direction="in"/>\
|
||||
<arg name="protocol" type="i" direction="in"/>\
|
||||
<arg name="name" type="s" direction="in"/>\
|
||||
<arg name="aprotocol" type="i" direction="in"/>\
|
||||
<arg name="flags" type="u" direction="in"/>\
|
||||
<arg name="interface" type="i" direction="out"/>\
|
||||
<arg name="protocol" type="i" direction="out"/>\
|
||||
<arg name="name" type="s" direction="out"/>\
|
||||
<arg name="aprotocol" type="i" direction="out"/>\
|
||||
<arg name="address" type="s" direction="out"/>\
|
||||
<arg name="flags" type="u" direction="out"/>\
|
||||
</method>\
|
||||
<method name="ResolveAddress">\
|
||||
<arg name="interface" type="i" direction="in"/>\
|
||||
<arg name="protocol" type="i" direction="in"/>\
|
||||
<arg name="address" type="s" direction="in"/>\
|
||||
<arg name="flags" type="u" direction="in"/>\
|
||||
<arg name="interface" type="i" direction="out"/>\
|
||||
<arg name="protocol" type="i" direction="out"/>\
|
||||
<arg name="aprotocol" type="i" direction="out"/>\
|
||||
<arg name="address" type="s" direction="out"/>\
|
||||
<arg name="name" type="s" direction="out"/>\
|
||||
<arg name="flags" type="u" direction="out"/>\
|
||||
</method>\
|
||||
<method name="ResolveService">\
|
||||
<arg name="interface" type="i" direction="in"/>\
|
||||
<arg name="protocol" type="i" direction="in"/>\
|
||||
<arg name="name" type="s" direction="in"/>\
|
||||
<arg name="type" type="s" direction="in"/>\
|
||||
<arg name="domain" type="s" direction="in"/>\
|
||||
<arg name="aprotocol" type="i" direction="in"/>\
|
||||
<arg name="flags" type="u" direction="in"/>\
|
||||
<arg name="interface" type="i" direction="out"/>\
|
||||
<arg name="protocol" type="i" direction="out"/>\
|
||||
<arg name="name" type="s" direction="out"/>\
|
||||
<arg name="type" type="s" direction="out"/>\
|
||||
<arg name="domain" type="s" direction="out"/>\
|
||||
<arg name="host" type="s" direction="out"/>\
|
||||
<arg name="aprotocol" type="i" direction="out"/>\
|
||||
<arg name="address" type="s" direction="out"/>\
|
||||
<arg name="port" type="q" direction="out"/>\
|
||||
<arg name="txt" type="aay" direction="out"/>\
|
||||
<arg name="flags" type="u" direction="out"/>\
|
||||
</method>\
|
||||
<method name="EntryGroupNew">\
|
||||
<arg name="path" type="o" direction="out"/>\
|
||||
</method>\
|
||||
<method name="DomainBrowserNew">\
|
||||
<arg name="interface" type="i" direction="in"/>\
|
||||
<arg name="protocol" type="i" direction="in"/>\
|
||||
<arg name="domain" type="s" direction="in"/>\
|
||||
<arg name="btype" type="i" direction="in"/>\
|
||||
<arg name="flags" type="u" direction="in"/>\
|
||||
<arg name="path" type="o" direction="out"/>\
|
||||
</method>\
|
||||
<method name="ServiceTypeBrowserNew">\
|
||||
<arg name="interface" type="i" direction="in"/>\
|
||||
<arg name="protocol" type="i" direction="in"/>\
|
||||
<arg name="domain" type="s" direction="in"/>\
|
||||
<arg name="flags" type="u" direction="in"/>\
|
||||
<arg name="path" type="o" direction="out"/>\
|
||||
</method>\
|
||||
<method name="ServiceBrowserNew">\
|
||||
<arg name="interface" type="i" direction="in"/>\
|
||||
<arg name="protocol" type="i" direction="in"/>\
|
||||
<arg name="type" type="s" direction="in"/>\
|
||||
<arg name="domain" type="s" direction="in"/>\
|
||||
<arg name="flags" type="u" direction="in"/>\
|
||||
<arg name="path" type="o" direction="out"/>\
|
||||
</method>\
|
||||
<method name="ServiceResolverNew">\
|
||||
<arg name="interface" type="i" direction="in"/>\
|
||||
<arg name="protocol" type="i" direction="in"/>\
|
||||
<arg name="name" type="s" direction="in"/>\
|
||||
<arg name="type" type="s" direction="in"/>\
|
||||
<arg name="domain" type="s" direction="in"/>\
|
||||
<arg name="aprotocol" type="i" direction="in"/>\
|
||||
<arg name="flags" type="u" direction="in"/>\
|
||||
<arg name="path" type="o" direction="out"/>\
|
||||
</method>\
|
||||
<method name="HostNameResolverNew">\
|
||||
<arg name="interface" type="i" direction="in"/>\
|
||||
<arg name="protocol" type="i" direction="in"/>\
|
||||
<arg name="name" type="s" direction="in"/>\
|
||||
<arg name="aprotocol" type="i" direction="in"/>\
|
||||
<arg name="flags" type="u" direction="in"/>\
|
||||
<arg name="path" type="o" direction="out"/>\
|
||||
</method>\
|
||||
<method name="AddressResolverNew">\
|
||||
<arg name="interface" type="i" direction="in"/>\
|
||||
<arg name="protocol" type="i" direction="in"/>\
|
||||
<arg name="address" type="s" direction="in"/>\
|
||||
<arg name="flags" type="u" direction="in"/>\
|
||||
<arg name="path" type="o" direction="out"/>\
|
||||
</method>\
|
||||
<method name="RecordBrowserNew">\
|
||||
<arg name="interface" type="i" direction="in"/>\
|
||||
<arg name="protocol" type="i" direction="in"/>\
|
||||
<arg name="name" type="s" direction="in"/>\
|
||||
<arg name="clazz" type="q" direction="in"/>\
|
||||
<arg name="type" type="q" direction="in"/>\
|
||||
<arg name="flags" type="u" direction="in"/>\
|
||||
<arg name="path" type="o" direction="out"/>\
|
||||
</method>\
|
||||
</interface>\
|
||||
</node>';
|
||||
|
||||
var Avahi = class Avahi {
|
||||
constructor() {
|
||||
const avahiProxy = Gio.DBusProxy.makeProxyWrapper(avahiInterface);
|
||||
this.proxy = new avahiProxy(
|
||||
Gio.DBus.system
|
||||
, 'org.freedesktop.Avahi'
|
||||
, '/'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let derp = new Avahi();
|
||||
let path = derp.proxy.ServiceBrowserNewSync(-1, -1, '_tcp', 'local', 0);
|
||||
var iface = 'org.freedesktop.Avahi.DomainBrowser';
|
||||
derp.proxy.connectSignal('ItemNew', function(proxy, result) {
|
||||
log(proxy);
|
||||
});
|
||||
derp.proxy.connectSignal('AllForNow', function(proxy, result) {
|
||||
log(proxy);
|
||||
});
|
||||
derp.proxy.connectSignal('StateChanged', function(proxy, result) {
|
||||
log(proxy);
|
||||
});
|
||||
*/
|
||||
//let loop = new GLib.MainLoop(null, false);
|
||||
//loop.run();
|
@ -0,0 +1,169 @@
|
||||
'use strict';
|
||||
|
||||
// TODO: Add manual server entries
|
||||
// TODO: Keyboard shortcuts
|
||||
const app_id = 'org.snapcast.control.gtk';
|
||||
|
||||
var run_local = false;
|
||||
try {
|
||||
pkg.initGettext();
|
||||
} catch(e) {
|
||||
imports.package.init({
|
||||
name: app_id
|
||||
, version: '0.0.1'
|
||||
, prefix: '/usr/local'
|
||||
, libdir: '/usr/local/lib/x86_64-linux-gnu'
|
||||
});
|
||||
const pkg = imports.package;
|
||||
run_local = true;
|
||||
}
|
||||
|
||||
pkg.id = app_id;
|
||||
pkg.initGettext();
|
||||
pkg.initFormat();
|
||||
pkg.require({
|
||||
'Gdk': '3.0'
|
||||
, 'Gio': '2.0'
|
||||
, 'GLib': '2.0'
|
||||
, 'GObject': '2.0'
|
||||
, 'Gtk': '3.0'
|
||||
});
|
||||
|
||||
const {Gdk, Gio, GLib, Gtk, GObject} = imports.gi;
|
||||
|
||||
const Util = imports.util;
|
||||
const AppWindow = imports.appwindow;
|
||||
|
||||
function initEnvironment() {
|
||||
window.getApp = function() {
|
||||
return Gio.Application.get_default();
|
||||
};
|
||||
}
|
||||
|
||||
const Application = GObject.registerClass(class SnapControl extends Gtk.Application {
|
||||
_init() {
|
||||
super._init({
|
||||
application_id: pkg.name
|
||||
, flags: (Gio.ApplicationFlags.CAN_OVERRIDE_APP_ID | Gio.ApplicationFlags.FLAGS_NONE)
|
||||
});
|
||||
GLib.set_application_name('Snap Control');
|
||||
Gtk.Window.set_default_icon_name(pkg.id);
|
||||
this.connect('startup', this._on_startup.bind(this));
|
||||
this.connect('activate', this._on_activate.bind(this));
|
||||
this.connect('shutdown', this._on_shutdown.bind(this));
|
||||
}
|
||||
|
||||
_on_startup() {
|
||||
this.load_css();
|
||||
Util.initActions(this, [{
|
||||
name: 'servers'
|
||||
, activate: this.showServersWindow.bind(this)
|
||||
}, {
|
||||
name: 'about'
|
||||
, activate: this.showAboutWindow.bind(this)
|
||||
}, {
|
||||
name: 'quit'
|
||||
, activate: (a, b) => {this.win.close()}
|
||||
}]);
|
||||
this._initAppMenu();
|
||||
}
|
||||
|
||||
_on_activate() {
|
||||
if (this.win === undefined) {
|
||||
this.win = new AppWindow.SnapControlWindow({ application: this });
|
||||
this.win.show_all();
|
||||
}
|
||||
this.win.present();
|
||||
}
|
||||
|
||||
_on_shutdown() {
|
||||
|
||||
}
|
||||
|
||||
load_css() {
|
||||
let css_provider = new Gtk.CssProvider();
|
||||
if (run_local) {
|
||||
let t = imports.searchPath[0].split('/');
|
||||
let p = t.splice(0, t.length - 1).concat(['data', 'application.css']);
|
||||
css_provider.load_from_path(GLib.DIR_SEPARATOR_S + GLib.build_filenamev(p));
|
||||
} else {
|
||||
let res_path = Gio.File.new_for_uri('resource:///org/snapcast/control/gtk/application.css');
|
||||
css_provider.load_from_file(res_path);
|
||||
}
|
||||
let screen = Gdk.Screen.get_default();
|
||||
let context = new Gtk.StyleContext();
|
||||
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||
}
|
||||
|
||||
_initAppMenu() {
|
||||
let builder = new Gtk.Builder();
|
||||
if (run_local) {
|
||||
let t = imports.searchPath[0].split('/');
|
||||
let p = t.splice(0, t.length - 1).concat(['data', 'app-menu.ui']);
|
||||
builder.add_from_file(GLib.DIR_SEPARATOR_S + GLib.build_filenamev(p));
|
||||
} else {
|
||||
builder.add_from_resource('/org/snapcast/control/gtk/app-menu.ui');
|
||||
}
|
||||
let menu = builder.get_object('app-menu');
|
||||
this.set_app_menu(menu);
|
||||
}
|
||||
|
||||
_getBuilderFilePath(filename) {
|
||||
let ret = filename;
|
||||
if (run_local) {
|
||||
let t = imports.searchPath[0].split('/');
|
||||
let p = t.splice(0, t.length - 1).concat(['data', filename]);
|
||||
ret = GLib.DIR_SEPARATOR_S + GLib.build_filenamev(p);
|
||||
} else {
|
||||
ret = 'resource:///org/snapcast/control/gtk/' + filename;
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
showAboutWindow() {
|
||||
let aboutdialog = new Gtk.AboutDialog({modal: true, transient_for: this.win});
|
||||
aboutdialog.set_program_name("%s %s".format(aboutdialog.get_program_name(), pkg.version));
|
||||
let about_comment = "";
|
||||
|
||||
//about_comment += _("A controller for the Snapcast multi-room audio system.");
|
||||
about_comment += _("A controller for the ") + '<a href="https://github.com/badaix/snapcast">Snapcast</a>' + (" multi-room audio system.");
|
||||
//about_comment += '\n' + _("GTK+") + ' %d.%d.%d'.format(Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version());
|
||||
let commentLabel = aboutdialog.get_template_child(Gtk.AboutDialog, 'comments_label');
|
||||
commentLabel.set_markup(about_comment);
|
||||
commentLabel.show();
|
||||
|
||||
aboutdialog.set_copyright("Copyright 2020 The Programmer.");
|
||||
aboutdialog.set_logo_icon_name(pkg.name);
|
||||
aboutdialog.set_website("https://github.com");
|
||||
aboutdialog.set_website_label(_("Homepage"));
|
||||
aboutdialog.set_license_type(Gtk.License.GPL_3_0);
|
||||
|
||||
aboutdialog.set_authors([
|
||||
"The Programmer <user@no.reply.github.com>"
|
||||
]);
|
||||
aboutdialog.add_credit_section('', []);
|
||||
aboutdialog.add_credit_section(_("Thanks to"), [
|
||||
'Snapcast https://github.com/badaix/snapcast'
|
||||
, ' Johannes Pohl https://github.com/badaix'
|
||||
]);
|
||||
|
||||
aboutdialog.connect('response', () => {aboutdialog.destroy()});
|
||||
aboutdialog.show();
|
||||
}
|
||||
|
||||
showServersWindow() {
|
||||
if (this.win) {
|
||||
this.win.showServersWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function main(argv) {
|
||||
initEnvironment();
|
||||
//window.getApp = () => Gio.Application.get_default();
|
||||
return (new Application()).run(argv);
|
||||
}
|
||||
|
||||
if (run_local) {
|
||||
main([]);
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
app_resource = gnome.compile_resources(app_id + '.src',
|
||||
app_id + '.src.gresource.xml',
|
||||
source_dir: '.',
|
||||
gresource_bundle: true,
|
||||
install: true,
|
||||
install_dir : pkgdatadir
|
||||
)
|
||||
|
||||
app_launcher = configure_file(
|
||||
output : app_id,
|
||||
input : app_id + '.in',
|
||||
configuration: app_configuration
|
||||
)
|
||||
|
||||
#install_data(app_launcher,
|
||||
# install_dir: get_option('bindir'),
|
||||
# install_mode: 'rwxr-xr-x'
|
||||
#)
|
||||
|
||||
install_data(app_launcher,
|
||||
install_dir: join_paths(get_option('datadir'), app_id),
|
||||
install_mode: 'rwxr-xr-x'
|
||||
)
|
@ -0,0 +1,8 @@
|
||||
#!@GJS@
|
||||
imports.package.init({
|
||||
name: "@PACKAGE_NAME@"
|
||||
, version: '@PACKAGE_VERSION@'
|
||||
, prefix: '@prefix@'
|
||||
, libdir: '@libdir@'
|
||||
});
|
||||
imports.package.run(imports.main);
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/snapcast/control/gtk/js">
|
||||
<file>main.js</file>
|
||||
<file>appwindow.js</file>
|
||||
<file>widgets.js</file>
|
||||
<file>util.js</file>
|
||||
<file>params.js</file>
|
||||
<file>snapcast.js</file>
|
||||
<file>avahi.js</file>
|
||||
</gresource>
|
||||
</gresources>
|
@ -0,0 +1,122 @@
|
||||
// Params:
|
||||
//
|
||||
// A set of convenience functions for dealing with pseudo-keyword
|
||||
// arguments.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// A function with complex arguments
|
||||
// function myFunction(params) {
|
||||
// params = Params.parse(params, { myFlags: Flags.NONE,
|
||||
// anInt: 42,
|
||||
// aString: 'hello, world!',
|
||||
// });
|
||||
// ... params.anInt, params.myFlags, params.aString ...
|
||||
// }
|
||||
// myFunction({ anInt: -1 });
|
||||
//
|
||||
// Extend a method to allow more params in a subclass
|
||||
// The superclass can safely use Params.parse(), it won't see
|
||||
// the extensions.
|
||||
// const MyClass = new Lang.Class({
|
||||
// ...
|
||||
// method: function(params) {
|
||||
// let mine = Params.filter(params, { anInt: 42 });
|
||||
// this.parent(params);
|
||||
// ... mine.anInt ...
|
||||
// }
|
||||
// });
|
||||
|
||||
// parse:
|
||||
// @params: caller-provided parameter object, or %null
|
||||
// @defaults: function-provided defaults object
|
||||
//
|
||||
// Examines @params and fills in default values from @defaults for
|
||||
// any properties in @defaults that don't appear in @params.
|
||||
// This function will throw a Error if @params contains a property
|
||||
// that is not recognized. Use fill() or filter() if you don't
|
||||
// want that.
|
||||
//
|
||||
// If @params is %null, this returns the values from @defaults.
|
||||
//
|
||||
// Return value: a new object, containing the merged parameters from
|
||||
// @params and @defaults
|
||||
function parse(params, defaults) {
|
||||
let ret = {}, prop;
|
||||
params = params || {};
|
||||
|
||||
for (prop in params) {
|
||||
if (!(prop in defaults))
|
||||
throw new Error('Unrecognized parameter "' + prop + '"');
|
||||
ret[prop] = params[prop];
|
||||
}
|
||||
|
||||
for (prop in defaults) {
|
||||
if (!(prop in params))
|
||||
ret[prop] = defaults[prop];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// fill:
|
||||
// @params: caller-provided parameter object, or %null
|
||||
// @defaults: function-provided defaults object
|
||||
//
|
||||
// Examines @params and fills in default values from @defaults
|
||||
// for any properties in @defaults that don't appear in @params.
|
||||
//
|
||||
// Differently from parse(), this function does not throw for
|
||||
// unrecognized parameters.
|
||||
//
|
||||
// Return value: a new object, containing the merged parameters from
|
||||
// @params and @defaults
|
||||
function fill(params, defaults) {
|
||||
let ret = {}, prop;
|
||||
params = params || {};
|
||||
|
||||
for (prop in params)
|
||||
ret[prop] = params[prop];
|
||||
|
||||
for (prop in defaults) {
|
||||
if (!(prop in ret))
|
||||
ret[prop] = defaults[prop];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// filter:
|
||||
// @params: caller-provided parameter object, or %null
|
||||
// @defaults: function-provided defaults object
|
||||
//
|
||||
// Examines @params and returns an object containing the
|
||||
// same properties as @defaults, but with values taken from
|
||||
// @params where available.
|
||||
// Then it removes from @params all matched properties.
|
||||
//
|
||||
// This is similar to parse(), but it accepts unknown properties
|
||||
// and modifies @params for known ones.
|
||||
//
|
||||
// If @params is %null, this returns the values from @defaults.
|
||||
//
|
||||
// Return value: a new object, containing the merged parameters from
|
||||
// @params and @defaults
|
||||
function filter(params, defaults) {
|
||||
let ret = {}, prop;
|
||||
params = params || {};
|
||||
|
||||
for (prop in defaults) {
|
||||
if (!(prop in params))
|
||||
ret[prop] = defaults[prop];
|
||||
}
|
||||
|
||||
for (prop in params) {
|
||||
if (prop in defaults) {
|
||||
ret[prop] = params[prop];
|
||||
delete params[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,177 @@
|
||||
'use strict';
|
||||
|
||||
const {Gio, GLib} = imports.gi;
|
||||
const Params = imports.params;
|
||||
const System = imports.system;
|
||||
|
||||
function initActions(actionMap, simpleActionEntries, context) {
|
||||
simpleActionEntries.forEach(function(entry) {
|
||||
let filtered = Params.filter(entry, {
|
||||
activate: null
|
||||
, change_state: null
|
||||
, context: null
|
||||
});
|
||||
let action = new Gio.SimpleAction(entry);
|
||||
|
||||
let context = filtered.context || actionMap;
|
||||
if (filtered.activate) {
|
||||
action.connect('activate', filtered.activate.bind(context));
|
||||
}
|
||||
if (filtered.change_state) {
|
||||
action.connect('change-state', filtered.change_state.bind(context));
|
||||
}
|
||||
|
||||
actionMap.add_action(action);
|
||||
});
|
||||
}
|
||||
|
||||
function toTitleCase(s) {
|
||||
s = s.replace(/[_-]+/g, " ");
|
||||
return s.replace( /\w\S*/g, function(t) {return t.charAt(0).toUpperCase() + t.substr(1).toLowerCase()} );
|
||||
};
|
||||
function time_ago(time) {
|
||||
|
||||
switch (typeof time) {
|
||||
case 'number':
|
||||
break;
|
||||
case 'string':
|
||||
time = +new Date(time);
|
||||
break;
|
||||
case 'object':
|
||||
if (time.constructor === Date) time = time.getTime();
|
||||
break;
|
||||
default:
|
||||
time = +new Date();
|
||||
}
|
||||
var time_formats = [
|
||||
[60, 'seconds', 1], // 60
|
||||
[120, '1 minute ago', '1 minute from now'], // 60*2
|
||||
[3600, 'minutes', 60], // 60*60, 60
|
||||
[7200, '1 hour ago', '1 hour from now'], // 60*60*2
|
||||
[86400, 'hours', 3600], // 60*60*24, 60*60
|
||||
[172800, 'Yesterday', 'Tomorrow'], // 60*60*24*2
|
||||
[604800, 'days', 86400], // 60*60*24*7, 60*60*24
|
||||
[1209600, 'Last week', 'Next week'], // 60*60*24*7*4*2
|
||||
[2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7
|
||||
[4838400, 'Last month', 'Next month'], // 60*60*24*7*4*2
|
||||
[29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4
|
||||
[58060800, 'Last year', 'Next year'], // 60*60*24*7*4*12*2
|
||||
[2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12
|
||||
[5806080000, 'Last century', 'Next century'], // 60*60*24*7*4*12*100*2
|
||||
[58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100
|
||||
];
|
||||
var seconds = (+new Date() - time) / 1000,
|
||||
token = 'ago',
|
||||
list_choice = 1;
|
||||
|
||||
if (seconds == 0) {
|
||||
return 'Just now'
|
||||
}
|
||||
if (seconds < 0) {
|
||||
seconds = Math.abs(seconds);
|
||||
token = 'from now';
|
||||
list_choice = 2;
|
||||
}
|
||||
var i = 0, format;
|
||||
while ((format = time_formats[i++]) !== undefined) {
|
||||
if (seconds < format[0]) {
|
||||
if (typeof format[2] == 'string')
|
||||
return format[list_choice];
|
||||
else
|
||||
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token;
|
||||
}
|
||||
}
|
||||
return time;
|
||||
}
|
||||
|
||||
function time_ago_sec(sec) {
|
||||
return time_ago(sec * 1000);
|
||||
}
|
||||
|
||||
function getAppFileInfo() {
|
||||
let stack = (new Error()).stack,
|
||||
stackLine = stack.split('\n')[1],
|
||||
coincidence, path, file;
|
||||
|
||||
if (!stackLine) throw new Error('Could not find current file (1)');
|
||||
|
||||
coincidence = new RegExp('@(.+):\\d+').exec(stackLine);
|
||||
if (!coincidence) throw new Error('Could not find current file (2)');
|
||||
|
||||
path = coincidence[1];
|
||||
file = Gio.File.new_for_path(path);
|
||||
return [file.get_path(), file.get_parent().get_path(), file.get_basename()];
|
||||
}
|
||||
|
||||
function getSettings(schemaId, path) {
|
||||
const GioSSS = Gio.SettingsSchemaSource;
|
||||
let schemaSource;
|
||||
|
||||
if (!pkg.moduledir.startsWith('resource://')) {
|
||||
// Running from the source tree
|
||||
schemaSource = GioSSS.new_from_directory(pkg.pkgdatadir,
|
||||
GioSSS.get_default(),
|
||||
false);
|
||||
} else {
|
||||
schemaSource = GioSSS.get_default();
|
||||
}
|
||||
|
||||
let schemaObj = schemaSource.lookup(schemaId, true);
|
||||
if (!schemaObj) {
|
||||
log('Missing GSettings schema ' + schemaId);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
if (path === undefined)
|
||||
return new Gio.Settings({ settings_schema: schemaObj });
|
||||
else
|
||||
return new Gio.Settings({
|
||||
settings_schema: schemaObj
|
||||
, path: path
|
||||
});
|
||||
}
|
||||
|
||||
function loadIcon(iconName, size) {
|
||||
let theme = Gtk.IconTheme.get_default();
|
||||
|
||||
return theme.load_icon(iconName,
|
||||
size,
|
||||
Gtk.IconLookupFlags.GENERIC_FALLBACK);
|
||||
}
|
||||
|
||||
function normalizeCasefoldAndUnaccent(str) {
|
||||
// The one and only!
|
||||
// Travelled all over gnome, from tracker to gnome-shell to gnome-control-center,
|
||||
// to seahorse, epiphany...
|
||||
//
|
||||
// Originally written by Aleksander Morgado <aleksander@gnu.org>
|
||||
|
||||
str = GLib.utf8_normalize(str, -1, GLib.NormalizeMode.NFKD);
|
||||
str = GLib.utf8_casefold(str, -1);
|
||||
|
||||
/* Combining diacritical mark?
|
||||
* Basic range: [0x0300,0x036F]
|
||||
* Supplement: [0x1DC0,0x1DFF]
|
||||
* For Symbols: [0x20D0,0x20FF]
|
||||
* Half marks: [0xFE20,0xFE2F]
|
||||
*/
|
||||
return str.replace(/[\u0300-\u036f]|[\u1dc0-\u1dff]|[\u20d0-\u20ff]|[\ufe20-\ufe2f]/, '');
|
||||
}
|
||||
|
||||
let t = imports.searchPath[0].split('/');
|
||||
let run_local = (t[t.length - 1] === 'src');
|
||||
|
||||
function getBuilderFile(filename) {
|
||||
let ret = filename;
|
||||
if (run_local) {
|
||||
let t = imports.searchPath[0].split('/');
|
||||
let p = t.splice(0, t.length - 1).concat(['data', filename]);
|
||||
let fp = GLib.DIR_SEPARATOR_S + GLib.build_filenamev(p);
|
||||
let file = Gio.file_new_for_path(fp);
|
||||
let [success, data, length] = file.load_contents(null);
|
||||
ret = data;
|
||||
} else {
|
||||
ret = 'resource:///org/snapcast/control/gtk/' + filename;
|
||||
}
|
||||
return ret;
|
||||
}
|
@ -0,0 +1,348 @@
|
||||
'use strict';
|
||||
|
||||
const {Gdk, Gio, GLib, Gtk, GObject} = imports.gi;
|
||||
|
||||
const Util = imports.util;
|
||||
|
||||
var GroupWidget = GObject.registerClass({
|
||||
Template: Util.getBuilderFile('group.ui')
|
||||
, InternalChildren: [
|
||||
'hBox'
|
||||
, 'iconState'
|
||||
, 'btnComboStream'
|
||||
, 'labelName'
|
||||
, 'scaleVolume'
|
||||
, 'btnConfig'
|
||||
, 'btnMute'
|
||||
]
|
||||
}, class GroupWidget extends Gtk.Box {
|
||||
_init(window, snapcast, group, params = []) {
|
||||
this.group = group;
|
||||
this.snapcast = snapcast;
|
||||
this.win = window;
|
||||
super._init(Object.assign({}, params));
|
||||
|
||||
//this._btnMute.set_active(this.group.muted);
|
||||
|
||||
this._btnConfig.connect('clicked', () => {
|
||||
let cw = new GroupOptions(this.win, this.snapcast, this.group);
|
||||
cw.show_all();
|
||||
});
|
||||
|
||||
this.snapcast.sServer.stream(this.group.streamID).connect('updated::status', this._set_stream_status.bind(this));
|
||||
this._set_stream_status(this.snapcast.sServer.stream(this.group.streamID));
|
||||
|
||||
this._labelName.set_text((this.group.name != this.group.streamID) ? this.group.name : '');
|
||||
|
||||
this.group.connect('updated::name', () => {
|
||||
this._labelName.set_text((this.group.name != this.group.streamID) ? this.group.name : '');
|
||||
});
|
||||
|
||||
this._btnMute.set_active(this.group.muted);
|
||||
this.group.connect('updated::muted', () => { this._btnMute.set_active(this.group.muted) });
|
||||
this._btnMute.connect('clicked', (b) => { this.group.muted = b.get_active() });
|
||||
//this.group.connect('updated::name', () => { this._labelName.set_text(this.group.name) });
|
||||
this.group.connect('destroy', () => { this.destroy() });
|
||||
this._btnComboStream.connect('changed', () => {
|
||||
if (this._btnComboStream.get_active_text() !== null){
|
||||
this.group.streamID = this._btnComboStream.get_active_text();
|
||||
}
|
||||
});
|
||||
this.snapcast.sServer.connect('clients::added', this._on_clients_added.bind(this));
|
||||
this.group.connect("updated::stream_id", () => {
|
||||
this.setComboActive();
|
||||
this._set_stream_status(this.snapcast.sServer.stream(this.group.streamID));
|
||||
});
|
||||
|
||||
this.setComboActive();
|
||||
//this.group.connect('updated::stream_id', this.setComboActive.bind(this));
|
||||
|
||||
this._scaleVolume.add_mark(0, Gtk.PositionType.BOTTOM, '');
|
||||
this._scaleVolume.add_mark(50, Gtk.PositionType.BOTTOM, '');
|
||||
this._scaleVolume.add_mark(100, Gtk.PositionType.BOTTOM, '');
|
||||
this._scaleVolume.set_value(this.group.volume);
|
||||
this._scaleVolume.connect('value-changed', () => {
|
||||
this.group.volume = Math.round(this._scaleVolume.get_value());
|
||||
});
|
||||
this.group.connect('updated::volume', () => { this._scaleVolume.set_value(this.group.volume) });
|
||||
|
||||
this.show_all();
|
||||
}
|
||||
|
||||
setComboActive() {
|
||||
this._btnComboStream.remove_all();
|
||||
this.snapcast.sServer.streams.forEach((stream, ii) => {
|
||||
this._btnComboStream.append_text(stream.id);
|
||||
if (stream.id == this.group.streamID) {
|
||||
this._btnComboStream.set_active(ii);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_set_stream_status(stream) {
|
||||
if (this._iconState === undefined || this._iconState === null) {
|
||||
return;
|
||||
}
|
||||
let _in = '';
|
||||
if (stream === undefined) {
|
||||
return;
|
||||
}
|
||||
switch (stream.status) {
|
||||
case 'playing':
|
||||
_in = 'gtk-media-play';
|
||||
break;
|
||||
case 'idle':
|
||||
default:
|
||||
_in = 'gtk-media-pause';
|
||||
break;
|
||||
}
|
||||
this._iconState.set_from_icon_name(_in, Gtk.IconSize.BUTTON);
|
||||
}
|
||||
|
||||
_on_clients_added(server, clients) {
|
||||
clients.forEach((client) => {
|
||||
if (client.groupID === this.group.id) {
|
||||
this.add(new ClientWidget(this.win, this.snapcast, this.group, client));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var GroupOptions = GObject.registerClass({
|
||||
Template: Util.getBuilderFile('group-options.ui')
|
||||
, InternalChildren: [
|
||||
'vBox'
|
||||
, 'bBox'
|
||||
, 'btnCancel'
|
||||
, 'btnSave'
|
||||
]
|
||||
}, class GroupOptions extends Gtk.Window {
|
||||
_init(window, snapcast, group, params = []) {
|
||||
super._init(Object.assign({
|
||||
application: window.get_application()
|
||||
, 'transient-for': window
|
||||
, 'attached-to': window
|
||||
}, params));
|
||||
|
||||
this.snapcast = snapcast;
|
||||
this.group = group;
|
||||
this._clientsByName = {};
|
||||
|
||||
this._btnSave.connect('clicked', () => { this._on_save() });
|
||||
this._btnCancel.connect('clicked', () => { this.close() });
|
||||
|
||||
this.snapcast.sServer._groups.forEach((group) => {
|
||||
group._clients.forEach((client) => {
|
||||
let cb = new Gtk.CheckButton();
|
||||
cb.set_label(client.name);
|
||||
cb.set_active(client.groupID == this.group.id);
|
||||
this._clientsByName[client.name] = client.id;
|
||||
this._vBox.add(cb);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_on_save() {
|
||||
var clients = [];
|
||||
this._vBox.foreach((child) => {
|
||||
if (child.get_active()) {
|
||||
clients.push(this._clientsByName[child.get_label()]);
|
||||
}
|
||||
});
|
||||
this.close();
|
||||
this.group.setClients(clients);
|
||||
}
|
||||
});
|
||||
|
||||
var ClientWidget = GObject.registerClass({
|
||||
Template: Util.getBuilderFile('client.ui')
|
||||
, InternalChildren: [
|
||||
'iconStatus'
|
||||
, 'labelName'
|
||||
, 'btnConfig'
|
||||
, 'btnMute'
|
||||
, 'scaleVolume'
|
||||
, 'labelVolume'
|
||||
, 'menuItemDetails'
|
||||
, 'menuItemRemove'
|
||||
]
|
||||
}, class ClientWidget extends Gtk.Box {
|
||||
_init(window, snapcast, group, client, params = []) {
|
||||
this.client = client;
|
||||
this.group = group;
|
||||
this.snapcast = snapcast;
|
||||
this.win = window;
|
||||
|
||||
super._init(Object.assign({
|
||||
orientation: Gtk.Orientation.VERTICAL
|
||||
, expand: false
|
||||
}, params));
|
||||
|
||||
this.client.connect('destroy', this.destroy.bind(this));
|
||||
|
||||
this._menuItemDetails.connect('activate', this._on_show_details.bind(this));
|
||||
this._menuItemRemove.connect('activate', this._on_delete.bind(this));
|
||||
|
||||
this.client.connect('updated::name', () => { this._labelName.set_text(this.client.name) });
|
||||
this.client.connect('updated::connected', () => { this._update() });
|
||||
this.client.connect('updated::disconnected', () => { this._update() });
|
||||
|
||||
this.client.connect('updated::muted', () => { this._btnMute.set_active(this.client.muted) });
|
||||
this._btnMute.connect('clicked', (b) => { this.client.muted = b.get_active() });
|
||||
|
||||
this._scaleVolume.add_mark(0, Gtk.PositionType.BOTTOM, '');
|
||||
this._scaleVolume.add_mark(50, Gtk.PositionType.BOTTOM, '');
|
||||
this._scaleVolume.add_mark(100, Gtk.PositionType.BOTTOM, '');
|
||||
|
||||
this._scaleVolume.connect('value-changed', () => {
|
||||
this._labelVolume.set_text(Math.round(this._scaleVolume.get_value()) + "%");
|
||||
this.client.volume = Math.round(this._scaleVolume.get_value());
|
||||
});
|
||||
this.client.connect('updated::volume', () => { this._scaleVolume.set_value(this.client.volume) });
|
||||
|
||||
this._update();
|
||||
|
||||
this.show_all();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
_update() {
|
||||
if (this.client.connected) {
|
||||
this._iconStatus.set_from_icon_name('gtk-yes', Gtk.IconSize.BUTTON);
|
||||
this._labelName.set_sensitive(true);
|
||||
} else {
|
||||
this._iconStatus.set_from_icon_name('gtk-no', Gtk.IconSize.BUTTON);
|
||||
this._labelName.set_sensitive(false);
|
||||
}
|
||||
this._labelName.set_text(this.client.name);
|
||||
this._btnMute.set_active(this.client.muted);
|
||||
this._scaleVolume.set_value(this.client.volume);
|
||||
}
|
||||
|
||||
_on_show_details() {
|
||||
log("_on_show_details");
|
||||
let modal, contentArea, button, actionArea;
|
||||
|
||||
let storeProps = new Gtk.ListStore ();
|
||||
storeProps.set_column_types ([
|
||||
GObject.TYPE_STRING
|
||||
, GObject.TYPE_STRING
|
||||
]);
|
||||
|
||||
let renderer = new Gtk.CellRendererText();
|
||||
let renderer2 = new Gtk.CellRendererText();
|
||||
|
||||
let column = new Gtk.TreeViewColumn({ title: "Item" });
|
||||
let column2 = new Gtk.TreeViewColumn({ title: "Value" });
|
||||
|
||||
let treeview = new Gtk.TreeView({
|
||||
model: storeProps
|
||||
, 'headers-visible': false
|
||||
});
|
||||
|
||||
column.pack_start(renderer, false);
|
||||
column.add_attribute(renderer, "text", 0);
|
||||
|
||||
column2.pack_start(renderer2, false);
|
||||
column2.add_attribute(renderer2, "text", 1);
|
||||
|
||||
treeview.append_column(column);
|
||||
treeview.append_column(column2);
|
||||
treeview.set_margin_top(10);
|
||||
treeview.set_margin_bottom(10);
|
||||
let sel = treeview.get_selection();
|
||||
sel.set_mode(Gtk.SelectionMode.NONE);
|
||||
//treeview.set_sensitive(false);
|
||||
|
||||
//storeProps.set(storeProps.append(), [0,1], ["Latency", this.client.props.config.latency]);
|
||||
storeProps.set(storeProps.append(), [0,1], ["ID:", this.client.id]);
|
||||
storeProps.set(storeProps.append(), [0,1], ["MAC:", this.client.props.host.mac]);
|
||||
storeProps.set(storeProps.append(), [0,1], ["IP:", this.client.props.host.ip]);
|
||||
storeProps.set(storeProps.append(), [0,1], ["Hostname:", this.client.props.host.name]);
|
||||
storeProps.set(storeProps.append(), [0,1], ["Operating System:", this.client.props.host.os]);
|
||||
storeProps.set(storeProps.append(), [0,1], ["Snapclient Version:", this.client.props.snapclient.version]);
|
||||
storeProps.set(storeProps.append(), [0,1], ["Last Seen:", Util.time_ago_sec(this.client.props.lastSeen.sec)]);
|
||||
|
||||
let adjustmentLatency = new Gtk.Adjustment({
|
||||
upper: 10000
|
||||
, lower: 0.0
|
||||
, 'step-increment': 1.0
|
||||
});
|
||||
let boxValues = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL
|
||||
, expand: true
|
||||
, spacing: 10
|
||||
});
|
||||
let boxLabels = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL
|
||||
, expand: true
|
||||
, spacing: 10
|
||||
});
|
||||
let boxItems = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL
|
||||
, expand: true
|
||||
, spacing: 10
|
||||
});
|
||||
boxItems.add(boxLabels);
|
||||
boxItems.add(boxValues);
|
||||
|
||||
let labelName = new Gtk.Label({ label: _("Name: "), expand: true });
|
||||
labelName.set_halign(Gtk.Align.START);
|
||||
this.entryName = new Gtk.Entry({
|
||||
text: this.client.name
|
||||
, expand: true
|
||||
});
|
||||
boxLabels.add(labelName);
|
||||
boxValues.add(this.entryName);
|
||||
|
||||
let labelLatency = new Gtk.Label({ label: _("Latency: "), expand: true });
|
||||
labelLatency.set_halign(Gtk.Align.START);
|
||||
this.btnSpinLatency = new Gtk.SpinButton({
|
||||
numeric: true
|
||||
, adjustment: adjustmentLatency
|
||||
, expand: true
|
||||
});
|
||||
this.btnSpinLatency.set_value(this.client.props.config.latency);
|
||||
boxLabels.add(labelLatency);
|
||||
boxValues.add(this.btnSpinLatency);
|
||||
|
||||
modal = new Gtk.Dialog({
|
||||
default_width: 250
|
||||
, modal: true
|
||||
, transient_for: this.win
|
||||
, title: _("Client Details: ") + this.client.name
|
||||
, use_header_bar: false
|
||||
});
|
||||
|
||||
contentArea = modal.get_content_area();
|
||||
contentArea.set_margin_top(10);
|
||||
contentArea.set_margin_right(10);
|
||||
contentArea.set_margin_bottom(10);
|
||||
contentArea.set_margin_left(10);
|
||||
contentArea.add(boxItems);
|
||||
contentArea.add(treeview);
|
||||
|
||||
button = Gtk.Button.new_with_label('OK');
|
||||
button.connect("clicked", () => {
|
||||
if (this.entryName.get_text() !== this.client.props.host.name || this.client.name !== this.entryName.get_text()) {
|
||||
this.client.name = this.entryName.get_text();
|
||||
}
|
||||
this.client.latency = this.btnSpinLatency.get_value();
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
actionArea = modal.get_action_area();
|
||||
actionArea.add(button);
|
||||
|
||||
modal.show_all();
|
||||
|
||||
}
|
||||
|
||||
_on_delete() {
|
||||
log("_on_delete");
|
||||
this.client.delete();
|
||||
}
|
||||
});
|
Loading…
Reference in new issue