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