Adding lightdm Aether theme
16
lightdm/themes/lightdm-webkit-theme-aether/.babelrc
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
'presets': [[
|
||||
'env',
|
||||
{
|
||||
'targets': {
|
||||
'browsers': [ "last 2 versions", "chrome >= 46" ]
|
||||
}
|
||||
}
|
||||
]],
|
||||
"plugins": [
|
||||
"babel-plugin-syntax-jsx",
|
||||
"jsx-control-statements",
|
||||
"transform-react-jsx",
|
||||
"transform-object-rest-spread"
|
||||
]
|
||||
}
|
||||
59
lightdm/themes/lightdm-webkit-theme-aether/.eslintrc
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"require": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:jsx-control-statements/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"jsx-control-statements"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-no-undef": [2, { "allowGlobals": true }],
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"no-unused-vars" : [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "_*"
|
||||
}
|
||||
],
|
||||
"no-warning-comments" : [
|
||||
"warn",
|
||||
{
|
||||
"terms": [
|
||||
"todo",
|
||||
"fixme",
|
||||
"hack"
|
||||
]
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
4
lightdm/themes/lightdm-webkit-theme-aether/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
**/node_modules/**
|
||||
**/.sass-cache/**
|
||||
|
||||
npm-debug.log
|
||||
1
lightdm/themes/lightdm-webkit-theme-aether/Aether
Symbolic link
@ -0,0 +1 @@
|
||||
./Aether
|
||||
339
lightdm/themes/lightdm-webkit-theme-aether/LICENSE
Normal file
@ -0,0 +1,339 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<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 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
174
lightdm/themes/lightdm-webkit-theme-aether/README.md
Normal file
@ -0,0 +1,174 @@
|
||||
# Aether
|
||||
###### ( lightdm-webkit-theme-aether )
|
||||
Inspired by a lifelong love with space.
|
||||
|
||||
A Sleek, straightforward Archlinux themed login screen written on lightdm and the lightdm-webkit2-greeter.
|
||||
|
||||
**Try it out [here, at the live demo](https://noisek.github.io/Aether/).**
|
||||
|
||||

|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Aether](#aether)
|
||||
- [Features](#features)
|
||||
- [Requirements](#requirements)
|
||||
- [Installation](#installation)
|
||||
- [Setting an Avatar Image](#setting-an-avatar-image)
|
||||
- [Using Your Own Wallpapers](#using-your-own-wallpapers)
|
||||
- [Modifying Date and Time Format](#modifying-date-and-time-format)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [My login screen hasn't changed!](#my-login-screen-hasnt-changed)
|
||||
- [My screen is black!](#my-screen-is-black)
|
||||
- [My system hangs at the boot screen!](#my-system-hangs-at-the-boot-screen)
|
||||
- [The lock screen isn't using my lightdm theme!](#the-lock-screen-isnt-using-my-lightdm-theme)
|
||||
- [Development](#development)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Building Project](#building-project)
|
||||
- [Monitoring Changes](#monitoring-changes)
|
||||
- [Todo](#todo)
|
||||
- [Credit](#credits)
|
||||
|
||||
## Features
|
||||
|
||||
**Stylish Default Themes**
|
||||
|
||||

|
||||
|
||||
**Advanced Customization**
|
||||
|
||||

|
||||
|
||||
**Multi User Support**
|
||||
|
||||

|
||||
|
||||
**Built-in Wallpaper Customization**
|
||||
|
||||

|
||||
|
||||
## Requirements
|
||||
- [lightdm-webkit2-greeter (aur/lightdm-webkit2-greeter )](https://github.com/Antergos/lightdm-webkit2-greeter)
|
||||
|
||||
## Installation
|
||||
|
||||
**Recommended Automatic Installation**
|
||||
|
||||
[Available on the AUR](https://aur.archlinux.org/packages/lightdm-webkit-theme-aether/). ArchLinux users can substitute pacaur with yaourt, packer, etc. as necessary and install with the following:
|
||||
|
||||
```
|
||||
pacaur -S lightdm-webkit-theme-aether
|
||||
```
|
||||
|
||||
**Manual Installation**
|
||||
|
||||
This assumes that you already have lightdm and lightdm-webkit2-greeter installed (but not configured).
|
||||
|
||||
```
|
||||
# If you prefer the last stable release, download from the releases page instead: https://github.com/NoiSek/Aether/releases/latest
|
||||
git clone git@github.com:NoiSek/Aether.git
|
||||
sudo cp --recursive Aether /usr/share/lightdm-webkit/themes/Aether
|
||||
|
||||
# Set default lightdm-webkit2-greeter theme to Aether
|
||||
sudo sed -i 's/^webkit_theme\s*=\s*\(.*\)/webkit_theme = lightdm-webkit-theme-aether #\1/g' /etc/lightdm/lightdm-webkit2-greeter.conf
|
||||
|
||||
# Set default lightdm greeter to lightdm-webkit2-greeter
|
||||
sudo sed -i 's/^\(#?greeter\)-session\s*=\s*\(.*\)/greeter-session = lightdm-webkit2-greeter #\1/ #\2g' /etc/lightdm/lightdm.conf
|
||||
```
|
||||
|
||||
### **Setting an Avatar Image**
|
||||
|
||||

|
||||
|
||||
Once LightDM, LightDM Webkit Greeter, and Aether are installed you will need to set an avatar image for your users. Size is irrelevant, and avatars will be displayed as a 125x125 circle (Yes, square images too). Users that don't have an avatar set will default to the [astronaut](./src/img/default-user.png).
|
||||
|
||||
To accomplish this, you can do either of the following:
|
||||
- Create an image in your home directory named `.face`.
|
||||
- Append `Icon=/path/to/your/avatar.png` to the bottom of the file at `/var/lib/AccountsService/users/<youraccountname>`
|
||||
|
||||
### **Using Your Own Wallpapers**
|
||||
|
||||
#### Method One:
|
||||
Add and delete wallpapers within the `src/img/wallpapers/` directory as you see fit. By default, you will find this folder at the absolute path: `/usr/share/lightdm-webkit/themes/lightdm-webkit-theme-aether/src/img/wallpapers/`.
|
||||
|
||||
#### Method Two:
|
||||
Edit the `background_images` value under `branding` within your lightdm-webkit config file located at `/etc/lightdm/lightdm-webkit2-greeter.conf`.
|
||||
*Note: This ignores the default value of /usr/share/backgrounds, as this is always set and would prevent the default wallpapers from working. To use wallpapers from within that directory, create a subdirectory at /usr/share/backgrounds/aether (or any other folder name) and change your config value accordingly.*
|
||||
|
||||
### **Modifying Date and Time Format**
|
||||
|
||||
The formatting symbols are not necessarily what you would expect them to be! See the following:
|
||||
|
||||
https://github.com/samsonjs/strftime#supported-specifiers
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### My login screen hasn't changed!
|
||||
|
||||
Make sure you have lightdm enabled via systemctl with `systemctl is-enabled lightdm.service`. If it isn't, follow up with:
|
||||
```
|
||||
sudo systemctl enable lightdm.service
|
||||
```
|
||||
|
||||
### My screen is black!
|
||||
|
||||
Verify that your libgl / glx drivers are properly installed. Find any potential issues with your X config by switching to another TTY with <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>F2</kbd> and trying:
|
||||
```
|
||||
sudo cat /var/log/Xorg.0.log | grep -i "glx"
|
||||
```
|
||||
|
||||
Are you able to run `glxinfo` without errors?
|
||||
|
||||
### My system hangs at the boot screen!
|
||||
|
||||
Switch to another TTY with <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>F2</kbd> and check your lightdm logs by running:
|
||||
```
|
||||
sudo tail /var/log/lightdm/seat0-greeter.log
|
||||
```
|
||||
|
||||
If you see something similar to:
|
||||
```
|
||||
*** (lightdm:709): CRITICAL **: session_get_login1_session_id: assertion 'session != NULL' failed
|
||||
```
|
||||
|
||||
Then you should try re-installing and / or reconfiguring your graphics drivers, especially if this occurred after a kernel update.
|
||||
|
||||
### The lock screen isn't using my lightdm theme!
|
||||
|
||||
If you are using cinnamon, gnome, or any gnome derivative; Good Luck. The solution involves [light-locker (community/light-locker)](https://github.com/the-cavalry/light-locker), but conflicts with the existing lock / screensaver applications. There is no known way to resolve this.
|
||||
|
||||
If you are not using a gnome derivative, see below.
|
||||
|
||||
Solution:
|
||||
|
||||
```
|
||||
echo "light-locker &" >> ~/.xprofile
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Make sure you have [Node](https://nodejs.org/en/) installed.
|
||||
|
||||
- `npm install` *(While in project directory)*
|
||||
|
||||
### Running Tests
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
### Building Project
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Monitoring Changes
|
||||
```
|
||||
npm run watch
|
||||
```
|
||||
|
||||
##### Credit
|
||||
- *Bear by Yu luck from the Noun Project*
|
||||
- *Power by Nikita Kozin from the Noun Project*
|
||||
- *Arrow by Landan Lloyd from the Noun Project*
|
||||
- Implements [Draggable](https://github.com/bcherny/draggable) by [bcherny](https://github.com/bcherny)
|
||||
- Implements [React-Color](https://github.com/casesandberg/react-color) by [bcherny](https://github.com/casesandberg)
|
||||
30
lightdm/themes/lightdm-webkit-theme-aether/dist/js/Aether.js
vendored
Normal file
1
lightdm/themes/lightdm-webkit-theme-aether/dist/js/Aether.js.map
vendored
Normal file
2
lightdm/themes/lightdm-webkit-theme-aether/dist/js/draggable.min.js
vendored
Normal file
86
lightdm/themes/lightdm-webkit-theme-aether/index.html
Normal file
@ -0,0 +1,86 @@
|
||||
<!doctype html>
|
||||
<html id="root">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Amazing Content</title>
|
||||
</head>
|
||||
<style>
|
||||
#preloader {
|
||||
background: rgb(37, 37, 37);
|
||||
position: absolute;
|
||||
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
z-index: 99999;
|
||||
|
||||
transition: background 1s cubic-bezier(0.4, 0.0, 0.2, 1), top 2s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
}
|
||||
|
||||
#preloader.loaded {
|
||||
background: rgba(0,0,0,0);
|
||||
top: 100vh;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div id="preloader"></div>
|
||||
|
||||
<div class="wallpaper-background">
|
||||
<div id="experimental-mount"></div>
|
||||
<div class="wallpaper-foreground"></div>
|
||||
<div class="wallpaper-preload"></div>
|
||||
</div>
|
||||
|
||||
<div class="notifications-container">
|
||||
</div>
|
||||
|
||||
<div class="login-window-container">
|
||||
<div id="login-window-mount"></div>
|
||||
<div id="date-display" class="date-display"></div>
|
||||
</div>
|
||||
|
||||
<div id="settings" class="settings hidden"></div>
|
||||
|
||||
<div id="settings-toggler-mount"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.__debug = false;
|
||||
|
||||
if (window.__debug === true) {
|
||||
window.lightdm = {
|
||||
"authenticate": function(){ return true; },
|
||||
"hibernate": function(){ return true; },
|
||||
"restart": function(){ return true; },
|
||||
"shutdown": function(){ return true; },
|
||||
"suspend": function(){ return true; },
|
||||
"can_hibernate": true,
|
||||
"can_restart": true,
|
||||
"can_shutdown": true,
|
||||
"can_suspend": true,
|
||||
"hostname": "Mercury",
|
||||
"default_session": 'deepin',
|
||||
"lock_hint": false,
|
||||
"users": [
|
||||
{"display_name":"captain","language":"en_US.utf8","real_name":"John Doe","layout":null,"image":"src/img/default-user.png","home_directory":"/home/captain","name":"captain","logged_in":false,"session":"deepin"},
|
||||
{"display_name":"GClooney","language":"en_US.utf8","real_name":"George Clooney","layout":null,"image":"src/img/default-user.png","home_directory":"/home/gclooney","name":"gclooney","logged_in":false,"session":"xfce"},
|
||||
{"display_name":"LannisterX","language":"en_US.utf8","real_name":"Jaime Lannister","layout":null,"image":"src/img/default-user.png","home_directory":"/home/lannisterx","name":"lannisterx","logged_in":false,"session":"gnome"}
|
||||
],
|
||||
"sessions": [
|
||||
{"name":"Deepin","key":"deepin","comment":"Deepin Desktop Environment"},
|
||||
{"name":"Gnome","key":"gnome","comment":"Gnome Desktop Environment"},
|
||||
{"name":"XFCE4","key":"xfce","comment":"XFCE Desktop Environment"},
|
||||
{"name":"Cinnamon","key":"cinnamon","comment":"Cinnamon DE"},
|
||||
{"name":"Cinnamon (Software)","key":"cinnamon-software","comment":"Cinnamon DE (Software Rendering)"},
|
||||
{"name":"LXDE","key":"lxde","comment":"LXDE Desktop Environment"}
|
||||
]
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<script src="dist/js/Aether.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
6
lightdm/themes/lightdm-webkit-theme-aether/index.theme
Normal file
@ -0,0 +1,6 @@
|
||||
[theme]
|
||||
name=Aether
|
||||
description=Aether, an Archlinux LightDM Theme.
|
||||
engine=lightdm-webkit-greeter
|
||||
url=index.html
|
||||
session=cinnamon
|
||||
9328
lightdm/themes/lightdm-webkit-theme-aether/package-lock.json
generated
Normal file
61
lightdm/themes/lightdm-webkit-theme-aether/package.json
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "lightdm-webkit-aether",
|
||||
"version": "1.0.0",
|
||||
"description": "Elegant LightDM Webkit theme.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "./node_modules/.bin/eslint src/es6/**",
|
||||
"build": "./node_modules/.bin/webpack-cli --progress --colors --env.production",
|
||||
"watch": "./node_modules/.bin/webpack-cli --progress --colors --watch --env.development"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/noisek/Aether.git"
|
||||
},
|
||||
"keywords": [
|
||||
"webkit",
|
||||
"aether",
|
||||
"lightdm",
|
||||
"login",
|
||||
"manager",
|
||||
"archlinux",
|
||||
"arch"
|
||||
],
|
||||
"author": "Noi Sek",
|
||||
"license": "GPL-3.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/noisek/Aether/issues"
|
||||
},
|
||||
"homepage": "https://github.com/noisek/Aether#readme",
|
||||
"dependencies": {
|
||||
"babel-core": "~6.26.0",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"css-loader": "^0.28.11",
|
||||
"cxs": "^6.2.0",
|
||||
"eslint": "~4.19.1",
|
||||
"eslint-loader": "^2.0.0",
|
||||
"eslint-plugin-jsx-control-statements": "^2.2.0",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"jquery": "^3.3.1",
|
||||
"jsx-control-statements": "^3.2.8",
|
||||
"node-sass": "^4.9.0",
|
||||
"pixi.js": "^4.7.3",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.0.0",
|
||||
"react-color": "^2.14.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"reactcss": "^1.2.2",
|
||||
"redux": "~4.0.0",
|
||||
"sass-loader": "^7.0.1",
|
||||
"strftime": "^0.10.0",
|
||||
"style-loader": "^0.21.0",
|
||||
"svg-inline-loader": "^0.8.0",
|
||||
"tinycolor2": "^1.4.1",
|
||||
"webpack": "~4.8.2",
|
||||
"webpack-cli": "^2.1.3"
|
||||
}
|
||||
}
|
||||
1077
lightdm/themes/lightdm-webkit-theme-aether/src/css/style.css
Normal file
BIN
lightdm/themes/lightdm-webkit-theme-aether/src/img/arch-logo.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
@ -0,0 +1,9 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="28px" height="28px" viewBox="626 306 28 28" enable-background="new 626 306 28 28" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<circle cx="640" cy="320" />
|
||||
<polygon fill="#FFFFFF" points="636.418,314.791 645.535,320.326 636.418,325.861 638.372,320.326 "/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 401 B |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 175 B |
BIN
lightdm/themes/lightdm-webkit-theme-aether/src/img/gl/smoke.png
Normal file
|
After Width: | Height: | Size: 615 B |
BIN
lightdm/themes/lightdm-webkit-theme-aether/src/img/gl/spark.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
lightdm/themes/lightdm-webkit-theme-aether/src/img/hibernate.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
@ -0,0 +1,9 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="95px" height="90.5px" viewBox="3 31 95 90.5" enable-background="new 3 31 95 90.5" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M94,82.983L98,77l-5.655-3.064l-2.017-5.396c-5.227-3.703-11.518-6.015-17.23-8.881c-4.047-2.031-8.242-4.455-12.881-4.638
|
||||
c-4.547-0.179-9.115,0.845-13.687,0.832c-4.545-0.014-9.225-0.322-13.687-0.177c-5.135,0.167-11.586,0.927-15.257,4.951
|
||||
C11.58,67.211,3,97,3,97l10,6l2-3l-2.952-3.445l12.727-7.944L34,103h11l19.071-14.362L77,103h11v-3l-4-1l-6-8v-9L94,82.983z
|
||||
M42.712,98.97l-4.208-4.532v-9.226h10.925L42.712,98.97z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 685 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
BIN
lightdm/themes/lightdm-webkit-theme-aether/src/img/reboot.png
Normal file
|
After Width: | Height: | Size: 512 B |
@ -0,0 +1,8 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="95px" height="94.964px" viewBox="264.14 221.317 95 94.964" enable-background="new 264.14 221.317 95 94.964"
|
||||
xml:space="preserve">
|
||||
<path stroke="currentColor" stroke-width="1px" d="M357.133,255.171c-5.879-19.633-24.02-33.854-45.493-33.854c-26.233,0-47.5,21.242-47.5,47.476
|
||||
c0,26.234,21.267,47.488,47.5,47.488s47.5-21.224,47.5-47.458c0-1.568-0.078-3.257-0.227-4.605h-5.393
|
||||
c0.167,1.348,0.256,3.028,0.256,4.599c0,23.271-18.865,42.113-42.137,42.113s-42.137-18.877-42.137-42.149
|
||||
s18.866-42.143,42.137-42.143c14.922,0,28.031,7.756,35.517,19.458l-15.188,0.005l18.711,6.839"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 716 B |
BIN
lightdm/themes/lightdm-webkit-theme-aether/src/img/shutdown.png
Normal file
|
After Width: | Height: | Size: 471 B |
@ -0,0 +1,11 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="95px" height="102.935px" viewBox="2.501 -1.468 95 102.935" enable-background="new 2.501 -1.468 95 102.935"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path d="M83.601,20.359c-1.124-1.122-2.322-2.202-3.555-3.212l-5.1,6.241c1.031,0.839,2.026,1.742,2.962,2.672
|
||||
c15.384,15.388,15.384,40.428,0,55.815c-15.391,15.387-40.424,15.387-55.815,0C6.708,66.489,6.708,41.449,22.09,26.064
|
||||
c0.91-0.91,1.873-1.781,2.86-2.591l-5.112-6.229c-1.188,0.976-2.344,2.025-3.441,3.12c-18.529,18.53-18.529,48.678,0,67.208
|
||||
c9.266,9.265,21.435,13.896,33.604,13.896s24.339-4.631,33.604-13.896C102.133,69.041,102.133,38.893,83.601,20.359z"/>
|
||||
<rect x="45.523" y="-1.468" width="8.058" height="42.072"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 809 B |
BIN
lightdm/themes/lightdm-webkit-theme-aether/src/img/sleep.png
Normal file
|
After Width: | Height: | Size: 513 B |
16
lightdm/themes/lightdm-webkit-theme-aether/src/img/sleep.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="95px" height="85.665px" viewBox="353.753 183.973 95 85.665" enable-background="new 353.753 183.973 95 85.665"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path d="M399.478,269.638h-45.725l1.771-9.368l36.217-46.844h-25.31l2.47-11.932h43.626l-1.91,9.416l-36.449,46.797h27.78
|
||||
L399.478,269.638z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M420.108,243.843h-11.43l0.443-2.343l9.053-11.711h-6.326l0.618-2.983h10.908l-0.478,2.354l-9.112,11.699h6.945
|
||||
L420.108,243.843z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M442.228,218.045h-22.862l0.885-4.684l18.108-23.422h-12.655l1.235-5.966h21.813l-0.956,4.708l-18.225,23.398h13.89
|
||||
L442.228,218.045z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 732 B |
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 350 KiB |
|
After Width: | Height: | Size: 879 KiB |
|
After Width: | Height: | Size: 566 KiB |
|
After Width: | Height: | Size: 586 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 974 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 824 KiB |
|
After Width: | Height: | Size: 747 KiB |
|
After Width: | Height: | Size: 893 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 602 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 880 KiB |
@ -0,0 +1,74 @@
|
||||
// DateDisplay -> Required by Main
|
||||
// --------------------------------------
|
||||
// Displays date below the login window.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Strftime from 'strftime';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
class DateDisplay extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
"initialized": false,
|
||||
"formattedDate": "",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
componentWillMount() {
|
||||
// Wait two seconds, so that the clock can render first and they fade in sequentially rather than in parallel.
|
||||
setTimeout(() => {
|
||||
this.setDate();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
|
||||
setDate() {
|
||||
this.setState({
|
||||
"initialized": true,
|
||||
"formattedDate": Strftime(this.props.settings.date_format)
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setDate();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let dateClasses = ['date'];
|
||||
let dateString = this.state.formattedDate;
|
||||
|
||||
if (this.state.initialized === true && this.props.settings.date_enabled === true) {
|
||||
dateClasses.push('loaded');
|
||||
} else if (this.state.date_enabled === false) {
|
||||
dateClasses.push('invisible');
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div className={ dateClasses.join(' ') } dangerouslySetInnerHTML={{ "__html": dateString }} />,
|
||||
document.getElementById("date-display")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DateDisplay.propTypes = {
|
||||
'settings': PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(DateDisplay);
|
||||
@ -0,0 +1,205 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as PIXI from 'pixi.js';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Particle from './Particle';
|
||||
|
||||
import { interpolatePoints, scale, randomRange } from 'Utils/Utils';
|
||||
|
||||
|
||||
const GradientGenerator = (start, end) => {
|
||||
const calculatedColors = {};
|
||||
|
||||
let RGBStart = PIXI.utils.hex2rgb(start);
|
||||
let RGBEnd = PIXI.utils.hex2rgb(end);
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
let percentage = i / 100;
|
||||
|
||||
let [ A_R, A_G, A_B ] = RGBStart;
|
||||
let [ B_R, B_G, B_B ] = RGBEnd;
|
||||
|
||||
let thisColor = [
|
||||
scale(A_R, B_R, percentage),
|
||||
scale(A_G, B_G, percentage),
|
||||
scale(A_B, B_B, percentage)
|
||||
];
|
||||
|
||||
calculatedColors[i] = PIXI.utils.rgb2hex(thisColor);
|
||||
}
|
||||
|
||||
return (percentage) => {
|
||||
return calculatedColors[Math.floor(percentage * 100)];
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
class ExperimentalStars extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
this.nodes = {};
|
||||
|
||||
// Pixi objects
|
||||
this.application = undefined;
|
||||
this.stars = [];
|
||||
this.sparks = [];
|
||||
|
||||
this.gradientGenerator = undefined;
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
this.application = new PIXI.Application({
|
||||
'height': window.innerHeight,
|
||||
'width': window.innerWidth,
|
||||
'transparent': true,
|
||||
'resolution': this.props.settings.page_zoom
|
||||
});
|
||||
|
||||
this.application.autoResize = true;
|
||||
|
||||
this.nodes.screen.appendChild(this.application.view);
|
||||
|
||||
setTimeout(() => {
|
||||
this.startAnimation();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
startAnimation() {
|
||||
let circle = new PIXI.Graphics();
|
||||
|
||||
circle.beginFill(0xFFFFFF, 1);
|
||||
circle.drawCircle(1, 1, 1);
|
||||
circle.cacheAsBitmap = true;
|
||||
|
||||
const starCount = 3;
|
||||
const sparkCount = 100; // Per star
|
||||
const sparkMinDecay = 750; // milliseconds
|
||||
const sparkMaxDecay = 1250; // milliseconds
|
||||
const sparkStartScale = 0.001;
|
||||
const sparkEndScale = 0.1;
|
||||
const startColor = 0xd426e0;
|
||||
const endColor = 0xed7b39;
|
||||
|
||||
this.gradientGenerator = GradientGenerator(startColor, endColor);
|
||||
|
||||
// Generate stars
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
let circleInstance = circle.clone();
|
||||
|
||||
this.stars.push([
|
||||
// [X, Y] Velocity, Circle object, Delay
|
||||
[-(randomRange(1, 2, 2)), randomRange(2, 4, 2)],
|
||||
circleInstance,
|
||||
Number(new Date()) + randomRange(1000, 8000)
|
||||
]);
|
||||
|
||||
circleInstance.blendMode = PIXI.BLEND_MODES.SCREEN;
|
||||
circleInstance.alpha = randomRange(0.7, 1, 2);
|
||||
circleInstance.x = randomRange(0, window.innerWidth + (window.innerWidth * 0.2));
|
||||
circleInstance.y = -20;
|
||||
|
||||
circleInstance.lastX = circleInstance.x;
|
||||
circleInstance.lastY = circleInstance.y;
|
||||
|
||||
circleInstance.interpolate = (ratio) => {
|
||||
return interpolatePoints(
|
||||
{
|
||||
'x': circleInstance.lastX,
|
||||
'y': circleInstance.lastY
|
||||
},
|
||||
{
|
||||
'x': circleInstance.x,
|
||||
'y': circleInstance.y
|
||||
},
|
||||
ratio
|
||||
);
|
||||
};
|
||||
|
||||
this.application.stage.addChild(circleInstance);
|
||||
}
|
||||
|
||||
|
||||
// Generate sparks
|
||||
let sparkTexture = PIXI.Texture.fromImage('src/img/gl/spark.png');
|
||||
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
// Generate a new self managing particle instance
|
||||
let options = {
|
||||
'parent': this.stars[i],
|
||||
'startColor': startColor,
|
||||
'endColor': endColor,
|
||||
'minDecay': sparkMinDecay,
|
||||
'maxDecay': sparkMaxDecay,
|
||||
'startScale': sparkStartScale,
|
||||
'endScale': sparkEndScale,
|
||||
'gradientGenerator': this.gradientGenerator
|
||||
};
|
||||
|
||||
for (let _i = 0; _i < sparkCount; _i++) {
|
||||
this.sparks[i] = this.sparks[i] || [];
|
||||
this.sparks[i].push(new Particle(sparkTexture, this.application, options));
|
||||
}
|
||||
}
|
||||
|
||||
//let smoke = PIXI.Sprite.fromImage('src/img/gl/smoke.png');
|
||||
|
||||
this.application.ticker.add(() => {
|
||||
const now = Number(new Date());
|
||||
|
||||
for (let i = 0; i < this.stars.length; i++) {
|
||||
let currentItem = this.stars[i];
|
||||
let [ velocity, object, startTime ] = currentItem;
|
||||
|
||||
if (now > startTime) {
|
||||
object.lastX = object.x;
|
||||
object.lastY = object.y;
|
||||
object.x += velocity[0];
|
||||
object.y += velocity[1];
|
||||
|
||||
if (object.x < -10 || object.y > window.innerHeight + 10) {
|
||||
object.alpha = randomRange(0.5, 1, 2);
|
||||
|
||||
object.x = randomRange(0, window.innerWidth + (window.innerWidth * 0.2));
|
||||
object.y = -20;
|
||||
|
||||
object.lastX = object.x;
|
||||
object.lastY = object.y;
|
||||
|
||||
currentItem[0] = [-(randomRange(1, 2, 2)), randomRange(2, 4, 2)];
|
||||
currentItem[2] = now + randomRange(3000, 8000);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return ReactDOM.createPortal(
|
||||
<div className="" ref={ (ref) => this.nodes.screen = ref } />,
|
||||
document.getElementById("experimental-mount")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ExperimentalStars.propTypes = {
|
||||
'settings': PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(ExperimentalStars);
|
||||
@ -0,0 +1,63 @@
|
||||
import { scale, randomRange } from 'Utils/Utils';
|
||||
import * as PIXI from 'pixi.js';
|
||||
|
||||
|
||||
export default class Particle {
|
||||
constructor(texture, application, options) {
|
||||
this.sprite = new PIXI.Sprite(texture);
|
||||
this.application = application;
|
||||
this.options = options;
|
||||
|
||||
this.birthDate = undefined;
|
||||
this.recycle();
|
||||
|
||||
// Initialization
|
||||
this.sprite.anchor.set(0, 1);
|
||||
this.application.stage.addChild(this.sprite);
|
||||
this.application.ticker.add(this.step.bind(this));
|
||||
}
|
||||
|
||||
|
||||
recycle() {
|
||||
const parentVelocity = this.options.parent[0];
|
||||
const parentSprite = this.options.parent[1];
|
||||
|
||||
// Reset Particle
|
||||
this.birthDate = Number(new Date());
|
||||
this.velocity = [ -(parentVelocity[0]) / 100, -(parentVelocity[1]) / 100 ];
|
||||
this.lifetime = randomRange(this.options.minDecay, this.options.maxDecay, 0);
|
||||
this.elapsed = 0;
|
||||
|
||||
// Reset Sprite
|
||||
this.sprite.scale.set(this.options.startScale);
|
||||
this.sprite.alpha = 0.05;
|
||||
this.sprite.tint = this.options.startColor;
|
||||
this.sprite.rotation = randomRange(-10, 10, 0) * Math.PI / 180;
|
||||
|
||||
let ratio = randomRange(0.5, 1, 2);
|
||||
let interpolatedPosition = parentSprite.interpolate(ratio);
|
||||
|
||||
this.sprite.x = interpolatedPosition.x + 2;
|
||||
this.sprite.y = interpolatedPosition.y;
|
||||
}
|
||||
|
||||
|
||||
step() {
|
||||
this.elapsed += this.application.ticker.elapsedMS;
|
||||
|
||||
if (this.elapsed >= this.lifetime) {
|
||||
this.recycle();
|
||||
} else {
|
||||
const percentage = (this.elapsed / this.lifetime);
|
||||
|
||||
let scaledSize = scale(this.options.startScale, this.options.endScale, percentage, false);
|
||||
this.sprite.scale.set(scaledSize);
|
||||
|
||||
this.sprite.x += this.velocity[0];
|
||||
this.sprite.y += this.velocity[1];
|
||||
|
||||
this.sprite.alpha = (1 - percentage) / 10;
|
||||
this.sprite.tint = this.options.gradientGenerator(percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
// LoginWindow -> Required by Main
|
||||
// --------------------------------------
|
||||
// Style / Composition wrapper.
|
||||
|
||||
import cxs from 'cxs';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Sidebar from './Sidebar';
|
||||
import UserPicker from './UserPicker';
|
||||
import Settings from 'Components/Settings';
|
||||
import DateDisplay from 'Components/DateDisplay';
|
||||
import SettingsToggler from 'Components/SettingsToggler';
|
||||
import ExperimentalStars from 'Components/ExperimentalStars';
|
||||
|
||||
|
||||
class LoginWindow extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
document.getElementById('preloader').className += 'loaded';
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const settings = this.props.settings;
|
||||
|
||||
let style = cxs({
|
||||
"border-radius": settings.window_border_radius,
|
||||
"font-size": settings.window_font_size
|
||||
});
|
||||
|
||||
return [
|
||||
<div className={ `login-window ${ style }` } key='login-window'>
|
||||
<Sidebar />
|
||||
<UserPicker />
|
||||
</div>,
|
||||
|
||||
<DateDisplay key='date-display' />,
|
||||
<Settings key='settings-window' />,
|
||||
<SettingsToggler key='settings-button' />,
|
||||
<If condition={ this.props.settings.experimental_stars_enabled } key='experimental-stars-wrap' >
|
||||
<ExperimentalStars key='experimental-stars' />
|
||||
</If>,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LoginWindow.propTypes = {
|
||||
'settings': PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(LoginWindow);
|
||||
@ -0,0 +1,75 @@
|
||||
// Clock -> Required by Components/CommandPanel
|
||||
// --------------------------------------
|
||||
// Just a clock.
|
||||
|
||||
import React from 'react';
|
||||
import Strftime from 'strftime';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
class Clock extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
"initialized": false,
|
||||
"formattedTime": "",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
this.updateClock();
|
||||
this.setState({
|
||||
"initialized": true
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
updateClock() {
|
||||
this.setState({
|
||||
"formattedTime": Strftime(this.props.settings.time_format)
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateClock();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let classes = ['right', 'clock'];
|
||||
let currentTime = this.state.formattedTime;
|
||||
|
||||
if (this.state.initialized === true && this.props.settings.time_enabled === true) {
|
||||
classes.push('loaded');
|
||||
} else if (this.props.settings.time_enabled === false) {
|
||||
classes.push('invisible');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ classes.join(' ') }>
|
||||
{ currentTime }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Clock.propTypes = {
|
||||
'settings': PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(Clock);
|
||||
@ -0,0 +1,65 @@
|
||||
// CommandItem -> Required by Components/CommandPanel/CommandList
|
||||
// --------------------------------------
|
||||
// CommandList item.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import cxs from 'cxs';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
export const SVGMap = {
|
||||
'hibernate': require('img/hibernate.svg'),
|
||||
'reboot': require('img/reboot.svg'),
|
||||
'shutdown': require('img/shutdown.svg'),
|
||||
'sleep': require('img/sleep.svg')
|
||||
};
|
||||
|
||||
|
||||
export const Item = ({ command, handleCommand, settings }) => {
|
||||
let disabled = command.toLowerCase().split('.')[1] || false;
|
||||
command = command.toLowerCase().split('.')[0];
|
||||
|
||||
let classes = ['command', command, disabled].filter((e) => e);
|
||||
let iconWrapperClasses = ['icon-wrapper'];
|
||||
|
||||
if (settings.command_icons_enabled === false) {
|
||||
iconWrapperClasses.push('hidden');
|
||||
}
|
||||
|
||||
let iconStyle = cxs({
|
||||
"color": settings.style_command_icon_color
|
||||
});
|
||||
|
||||
iconWrapperClasses.push(iconStyle);
|
||||
|
||||
let textStyle = cxs({
|
||||
"color": settings.style_command_text_color,
|
||||
"text-align": settings.command_text_align
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ classes.join(' ') } onClick={ handleCommand.bind(this, command, disabled) }>
|
||||
<div className={ iconWrapperClasses.join(' ') } dangerouslySetInnerHTML={{ "__html": SVGMap[command] }} />
|
||||
<div className={ `text ${ textStyle }` }>{ command }</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Item.propTypes = {
|
||||
'command': PropTypes.string.isRequired,
|
||||
'handleCommand': PropTypes.func.isRequired,
|
||||
'settings': PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(Item);
|
||||
@ -0,0 +1,34 @@
|
||||
// CommandList -> Required by CommandPanel
|
||||
// --------------------------------------
|
||||
// Displays system commands.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import Item from './Item';
|
||||
|
||||
|
||||
export const List = ({ enabledCommands, handleCommand }) => {
|
||||
let items = enabledCommands.map((command) =>
|
||||
<Item
|
||||
key={ command }
|
||||
command={ command }
|
||||
handleCommand={ handleCommand }
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="commands-wrapper">
|
||||
{ items }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
List.propTypes = {
|
||||
'enabledCommands': PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
'handleCommand': PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
export default List;
|
||||
@ -0,0 +1,107 @@
|
||||
// CommandPanel -> Required by Main
|
||||
// --------------------------------------
|
||||
// The system management half of the greeter logic.
|
||||
// Displays system info and handles Sleep, Shutdown, etc.
|
||||
|
||||
import cxs from 'cxs';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import * as SystemOperations from 'Logic/SystemOperations';
|
||||
import WallpaperSwitcher from './WallpaperSwitcher';
|
||||
import Clock from './Clock';
|
||||
import List from './List';
|
||||
|
||||
|
||||
class Sidebar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
|
||||
handleCommand(command, disabled, event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (disabled !== false) {
|
||||
window.notifications.generate(`${ command } is disabled on this system.`, "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
SystemOperations.handleCommand(command);
|
||||
}
|
||||
|
||||
|
||||
getEnabledCommands() {
|
||||
let commands = {
|
||||
"Shutdown": (window.lightdm.can_shutdown && this.props.settings.command_shutdown_enabled),
|
||||
"Reboot": (window.lightdm.can_restart && this.props.settings.command_reboot_enabled),
|
||||
"Hibernate": (window.lightdm.can_hibernate && this.props.settings.command_hibernate_enabled),
|
||||
"Sleep": (window.lightdm.can_suspend && this.props.settings.command_sleep_enabled)
|
||||
};
|
||||
|
||||
// Filter out commands we can't execute.
|
||||
let enabledCommands = (
|
||||
Object.keys(commands)
|
||||
.map((key) => commands[key] ? key : false)
|
||||
.filter((command) => command !== false)
|
||||
);
|
||||
|
||||
// Are both hibernation and suspend disabled?
|
||||
// Add the row back and disable it so that the user is aware of what's happening.
|
||||
if (window.lightdm.can_suspend === false && window.lightdm.can_hibernate === false) {
|
||||
enabledCommands.push("Sleep.disabled");
|
||||
}
|
||||
|
||||
return enabledCommands;
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let settings = this.props.settings;
|
||||
|
||||
let hostname = window.lightdm.hostname;
|
||||
let hostnameClasses = ['left', 'hostname'];
|
||||
let hostNameDisabled = (settings.hostname_enabled === false);
|
||||
|
||||
let commands = this.getEnabledCommands();
|
||||
|
||||
if (hostNameDisabled) {
|
||||
hostnameClasses.push('invisible');
|
||||
}
|
||||
|
||||
let styles = cxs({
|
||||
'background': settings.style_command_background_color
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ `command-panel ${ styles }` }>
|
||||
<WallpaperSwitcher />
|
||||
<List
|
||||
enabledCommands={ commands }
|
||||
handleCommand={ this.handleCommand.bind(this) }
|
||||
/>
|
||||
<div className="bottom">
|
||||
<div className={ hostnameClasses.join(' ') }>{ hostname }</div>
|
||||
<Clock />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Sidebar.propTypes = {
|
||||
'settings': PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(Sidebar);
|
||||
@ -0,0 +1,220 @@
|
||||
// WallpaperSwitcher -> Required by Components/CommandPanel
|
||||
// --------------------------------------
|
||||
// Serves to handle wallpaper switching through DOM manipulation.
|
||||
|
||||
import cxs from 'cxs';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import * as FileOperations from 'Logic/FileOperations';
|
||||
import * as Settings from 'Logic/Settings';
|
||||
|
||||
const FADEOUT_TIME = 600;
|
||||
|
||||
|
||||
class WallpaperSwitcher extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let wallpaperDirectory = FileOperations.getWallpaperDirectory();
|
||||
let wallpapers = FileOperations.getWallpapers(wallpaperDirectory);
|
||||
|
||||
this.cyclerBackground = undefined;
|
||||
this.cyclerForeground = undefined;
|
||||
this.cyclerPreloader = undefined;
|
||||
|
||||
this.state = {
|
||||
"directory": wallpaperDirectory,
|
||||
"wallpapers": wallpapers,
|
||||
"selectedWallpaper": undefined,
|
||||
"savedWallpaper": undefined,
|
||||
"switcher": {
|
||||
"active": false,
|
||||
"currentlyFading": false,
|
||||
"index": 0
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
componentWillMount() {
|
||||
// Set background wallpaper
|
||||
let directory = this.state.directory;
|
||||
let image = Settings.requestSetting('wallpaper', 'space-1.jpg');
|
||||
this.cyclerBackground = document.querySelectorAll('.wallpaper-background')[0];
|
||||
this.cyclerForeground = document.querySelectorAll('.wallpaper-foreground')[0];
|
||||
this.cyclerPreloader = document.querySelectorAll('.wallpaper-preload')[0];
|
||||
|
||||
this.cyclerForeground.style.background = `url('${ directory }${ image }')`;
|
||||
this.cyclerForeground.style.backgroundSize = "cover";
|
||||
document.body.style.background = `url('${ directory }${ image }')`;
|
||||
document.body.style.backgroundSize = "cover";
|
||||
|
||||
this.setState({
|
||||
"savedWallpaper": image
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
acceptWallpaper() {
|
||||
let selectedWallpaper = this.state.selectedWallpaper;
|
||||
let switcher = this.state.switcher;
|
||||
|
||||
// Due diligence.
|
||||
Settings.saveSetting('wallpaper', selectedWallpaper);
|
||||
window.notifications.generate("This wallpaper has been saved as your default background.", 'success');
|
||||
|
||||
// Reset switcher state
|
||||
switcher.active = false;
|
||||
switcher.index = 0;
|
||||
|
||||
this.setState({
|
||||
"selectedWallpaper": selectedWallpaper,
|
||||
"savedWallpaper": selectedWallpaper,
|
||||
"switcher": switcher
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
cycleWallpaper() {
|
||||
// Prevent animation transitions stacking and causing issues.
|
||||
if (this.state.switcher.currentlyFading === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let wallpapers = this.state.wallpapers;
|
||||
let switcher = this.state.switcher;
|
||||
|
||||
const nextIndex = (index) => (index + wallpapers.length + 1) % wallpapers.length;
|
||||
|
||||
let newIndex = nextIndex(switcher.index);
|
||||
let newWallpaper = wallpapers[newIndex];
|
||||
|
||||
let preloadedIndex = nextIndex(newIndex);
|
||||
let preloadedWallpaper = wallpapers[preloadedIndex];
|
||||
|
||||
this.setWallpaper(newWallpaper, preloadedWallpaper);
|
||||
|
||||
switcher.index = newIndex;
|
||||
|
||||
this.setState({
|
||||
"switcher": switcher
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleSwitcherActivation() {
|
||||
let switcher = this.state.switcher;
|
||||
switcher.active = true;
|
||||
this.cycleWallpaper();
|
||||
|
||||
this.setState({
|
||||
"switcher": switcher
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
rejectWallpaper() {
|
||||
let savedWallpaper = this.state.savedWallpaper;
|
||||
let switcher = this.state.switcher;
|
||||
|
||||
// Reset switcher state
|
||||
switcher.active = false;
|
||||
switcher.index = 0;
|
||||
|
||||
this.setState({
|
||||
"switcher": switcher
|
||||
});
|
||||
|
||||
this.setWallpaper(savedWallpaper);
|
||||
|
||||
window.notifications.generate("Wallpaper reset to default, no changes saved.");
|
||||
}
|
||||
|
||||
|
||||
setWallpaper(newWallpaper, preloadedWallpaper=false) {
|
||||
let switcher = this.state.switcher;
|
||||
|
||||
// Fadeout foreground wallpaper to new wallpaper
|
||||
let directory = this.state.directory;
|
||||
this.cyclerBackground.style.background = `url('${ directory }${ newWallpaper }')`;
|
||||
this.cyclerBackground.style.backgroundSize = 'cover';
|
||||
this.cyclerForeground.className += " fadeout";
|
||||
|
||||
switcher.currentlyFading = true;
|
||||
|
||||
if (preloadedWallpaper !== false) {
|
||||
// Preload the next image
|
||||
this.cyclerPreloader.style.background = `url('${ directory }${ preloadedWallpaper }')`;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// Cycle new wallpaper back to the front, make it visible again.
|
||||
this.cyclerForeground.style.background = `url('${ directory }${ newWallpaper }')`;
|
||||
this.cyclerForeground.style.backgroundSize = 'cover';
|
||||
this.cyclerForeground.className = this.cyclerForeground.className.replace(" fadeout", "");
|
||||
document.body.style.background = `url('${ directory }${ newWallpaper }')`;
|
||||
document.body.style.backgroundSize = 'cover';
|
||||
|
||||
let switcher = this.state.switcher;
|
||||
switcher.currentlyFading = false;
|
||||
|
||||
this.setState({
|
||||
"selectedWallpaper": newWallpaper,
|
||||
"switcher": switcher
|
||||
});
|
||||
}, FADEOUT_TIME);
|
||||
}
|
||||
|
||||
|
||||
generateOptions() {
|
||||
let classes = ['options'];
|
||||
|
||||
if (this.state.switcher.active === true) {
|
||||
classes.push('active');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='options-wrapper'>
|
||||
<div className={ classes.join(' ') }>
|
||||
<div className="button-reject" onClick={ this.rejectWallpaper.bind(this) } >✕</div>
|
||||
<div className="button-accept" onClick={ this.acceptWallpaper.bind(this) } >✓</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let options = this.generateOptions();
|
||||
|
||||
let style = cxs({
|
||||
"background-image": `url(${ this.props.distroImage }) !important`
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="distro-wrapper">
|
||||
<div className={ `distro-logo ${ style }` } onClick={ this.handleSwitcherActivation.bind(this) }></div>
|
||||
{ options }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
WallpaperSwitcher.propTypes = {
|
||||
'distroImage': PropTypes.string
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'distroImage': state.settings.distro
|
||||
};
|
||||
},
|
||||
null
|
||||
)(WallpaperSwitcher);
|
||||
@ -0,0 +1,84 @@
|
||||
// UserPanelForm -> Required by Components/UserPanel
|
||||
// --------------------------------------
|
||||
// The form displayed within the User Panel to handle
|
||||
// user input for the authentication process.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import cxs from 'cxs';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import SessionDropdown from './SessionDropdown';
|
||||
import PasswordField from './PasswordField';
|
||||
|
||||
|
||||
const submitButton = require('img/arrow.svg');
|
||||
|
||||
|
||||
export const UserPanelForm = (props) => {
|
||||
let usernameClasses = ['user-username'];
|
||||
usernameClasses.push(cxs({
|
||||
"color": props.settings.style_login_username_color
|
||||
}));
|
||||
|
||||
let submitButtonClasses = ['submit-button'];
|
||||
submitButtonClasses.push(cxs({
|
||||
"color": props.settings.style_login_button_color
|
||||
}));
|
||||
|
||||
return (
|
||||
<form className="login-form" onSubmit={ props.handleLoginSubmit }>
|
||||
<div className={ usernameClasses.join(" ") }>{ props.activeUser.display_name }</div>
|
||||
<div className="user-password-container">
|
||||
<PasswordField
|
||||
password={ props.password }
|
||||
passwordFailed={ props.passwordFailed }
|
||||
handlePasswordInput={ props.handlePasswordInput }
|
||||
/>
|
||||
</div>
|
||||
<div className="submit-row-container">
|
||||
<div className="submit-row">
|
||||
<div className="left">
|
||||
<SessionDropdown
|
||||
activeSession={ props.activeSession }
|
||||
setActiveSession={ props.setActiveSession }
|
||||
/>
|
||||
</div>
|
||||
<div className="right">
|
||||
<label className={ submitButtonClasses.join(" ") }>
|
||||
<input type="submit" />
|
||||
<div dangerouslySetInnerHTML={{ "__html": submitButton }} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
UserPanelForm.propTypes = {
|
||||
'activeUser': PropTypes.object,
|
||||
'activeSession': PropTypes.object,
|
||||
'settings': PropTypes.object.isRequired,
|
||||
|
||||
'password': PropTypes.string.isRequired,
|
||||
'passwordFailed': PropTypes.bool.isRequired,
|
||||
|
||||
'handleLoginSubmit': PropTypes.func.isRequired,
|
||||
'handlePasswordInput': PropTypes.func.isRequired,
|
||||
'setActiveSession': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'activeUser': state.user,
|
||||
'activeSession': state.session,
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(UserPanelForm);
|
||||
@ -0,0 +1,280 @@
|
||||
// UserPanel -> Required by Main
|
||||
// --------------------------------------
|
||||
// The login management half of the greeter logic.
|
||||
|
||||
import cxs from 'cxs';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import UserSwitchButton from './UserSwitcher/UserSwitchButton';
|
||||
import UserSwitcher from './UserSwitcher';
|
||||
import UserPanelForm from './Form';
|
||||
|
||||
const FADE_IN_DURATION = 200;
|
||||
const ERROR_SHAKE_DURATION = 600;
|
||||
|
||||
const CTRL_KEYCODE = 17;
|
||||
const A_KEYCODE = 65;
|
||||
|
||||
|
||||
class UserPicker extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
"fadeIn": false,
|
||||
"password": "",
|
||||
"passwordFailed": false,
|
||||
"switcherActive": false,
|
||||
};
|
||||
|
||||
this.CTRL_Pressed = false;
|
||||
this.A_Pressed = false;
|
||||
}
|
||||
|
||||
|
||||
componentWillMount() {
|
||||
// Define functions required in the global scope by LightDM.
|
||||
window.show_prompt = (text, type) => {
|
||||
if (type === 'text') {
|
||||
window.notifications.generate(text);
|
||||
} else if (type === 'password') {
|
||||
window.lightdm.respond(this.state.password);
|
||||
}
|
||||
};
|
||||
|
||||
window.show_message = (text, type) => {
|
||||
window.notifications.generate(text, type);
|
||||
};
|
||||
|
||||
window.authentication_complete = () => {
|
||||
if (window.lightdm.is_authenticated) {
|
||||
window.lightdm.start_session_sync(this.props.activeSession.key);
|
||||
} else {
|
||||
this.rejectPassword();
|
||||
}
|
||||
};
|
||||
|
||||
window.autologin_timer_expired = () => {
|
||||
window.notifications.generate("Autologin expired.");
|
||||
};
|
||||
|
||||
// Add a handler for Ctrl+A to prevent selection issues.
|
||||
document.onkeydown = this.onKeyDown.bind(this);
|
||||
document.onkeyup = this.onKeyUp.bind(this);
|
||||
}
|
||||
|
||||
|
||||
onKeyDown(e) {
|
||||
if (e.keyCode === CTRL_KEYCODE) {
|
||||
this.CTRL_Pressed = true;
|
||||
}
|
||||
|
||||
if (e.keyCode === A_KEYCODE) {
|
||||
this.A_Pressed = true;
|
||||
}
|
||||
|
||||
if (this.CTRL_Pressed && this.A_Pressed) {
|
||||
e.preventDefault();
|
||||
|
||||
let target = document.getElementById('password-field');
|
||||
target.focus();
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onKeyUp(e) {
|
||||
if (e.keyCode === CTRL_KEYCODE) {
|
||||
this.CTRL_Pressed = false;
|
||||
}
|
||||
|
||||
if (e.keyCode === A_KEYCODE) {
|
||||
this.A_Pressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleLoginSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (window.__debug === true) {
|
||||
if (this.state.password.toLowerCase() !== 'password') {
|
||||
this.rejectPassword();
|
||||
} else {
|
||||
window.notifications.generate(`You are now logged in as ${ this.props.activeUser.display_name } to ${ this.props.activeSession.name }.`, 'success');
|
||||
this.setState({
|
||||
"password": ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
window.lightdm.authenticate(this.props.activeUser.username || this.props.activeUser.name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleSwitcherClick(event) {
|
||||
if (window.lightdm.users.length < 2) {
|
||||
window.notifications.generate("You are the only user that is able to log in on this system.", 'error');
|
||||
return false;
|
||||
} else if (window.lightdm.users.length === 2) {
|
||||
// No point in showing them the switcher if there is only one other user. Switch immediately.
|
||||
let otherUser = window.lightdm.users.filter((user) => {
|
||||
return user.username !== this.props.activeUser.username;
|
||||
})[0];
|
||||
|
||||
this.setActiveUser(otherUser, true);
|
||||
window.notifications.generate("User has been automatically switched to the only other user on this system.");
|
||||
} else {
|
||||
this.setState({
|
||||
"switcherActive": true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handlePasswordInput(event) {
|
||||
this.setState({
|
||||
"password": event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setActiveSession(session) {
|
||||
this.props.dispatch({
|
||||
'type': 'AUTH_SET_ACTIVE_SESSION',
|
||||
'session': session
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setActiveUser(user, isBypass) {
|
||||
this.props.dispatch({
|
||||
"type": 'AUTH_SET_ACTIVE_USER',
|
||||
"user": user
|
||||
});
|
||||
|
||||
// Fade in, except when switching between 1 of 2 users.
|
||||
if (isBypass === false || isBypass === undefined) {
|
||||
this.setState({
|
||||
"fadeIn": true,
|
||||
"switcherActive": false
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
"fadeIn": false
|
||||
});
|
||||
}, FADE_IN_DURATION);
|
||||
} else {
|
||||
this.setState({
|
||||
"switcherActive": false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
rejectPassword() {
|
||||
window.notifications.generate("Password incorrect, please try again.", 'error');
|
||||
|
||||
this.setState({
|
||||
"password": "",
|
||||
"passwordFailed": true
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
"passwordFailed": false
|
||||
});
|
||||
}, ERROR_SHAKE_DURATION);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let loginPanelClasses = ['login-panel-main'];
|
||||
let avatarClasses = ['avatar-container'];
|
||||
let avatarBackgroundClasses = ['avatar-background'];
|
||||
let settings = this.props.settings;
|
||||
|
||||
if (this.state.fadeIn === true) {
|
||||
loginPanelClasses.push('fadein');
|
||||
}
|
||||
|
||||
if (this.state.switcherActive === true) {
|
||||
loginPanelClasses.push('fadeout');
|
||||
}
|
||||
|
||||
if (settings.avatar_enabled === false) {
|
||||
avatarClasses.push('hidden');
|
||||
}
|
||||
|
||||
if (settings.avatar_background_enabled === false) {
|
||||
avatarBackgroundClasses.push('avatar-background-hidden');
|
||||
}
|
||||
|
||||
let _styles = {
|
||||
"background": `linear-gradient(to bottom, ${ settings.style_login_gradient_top_color } 0%, ${ settings.style_login_gradient_bottom_color } 100%)`,
|
||||
"border-color": settings.style_login_border_color
|
||||
};
|
||||
|
||||
if (settings.style_login_border_enabled === false) {
|
||||
_styles['border'] = 'none !important';
|
||||
}
|
||||
|
||||
let style = cxs(_styles);
|
||||
|
||||
return (
|
||||
<div className={ `user-panel ${ style }` }>
|
||||
<div className={ loginPanelClasses.join(' ') }>
|
||||
<div className={ avatarClasses.join(' ') }>
|
||||
<div className= { avatarBackgroundClasses.join(' ') }>
|
||||
<div className="avatar-mask">
|
||||
<img className="user-avatar" src={ this.props.activeUser.image } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UserPanelForm
|
||||
password={ this.state.password }
|
||||
passwordFailed={ this.state.passwordFailed }
|
||||
handleLoginSubmit={ this.handleLoginSubmit.bind(this) }
|
||||
handlePasswordInput={ this.handlePasswordInput.bind(this) }
|
||||
setActiveSession={ this.setActiveSession.bind(this) }
|
||||
/>
|
||||
<div className="bottom">
|
||||
<If condition={ settings.user_switcher_enabled }>
|
||||
<UserSwitchButton handleSwitcherClick={ this.handleSwitcherClick.bind(this) } />
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
<UserSwitcher
|
||||
active={ this.state.switcherActive }
|
||||
setActiveUser={ this.setActiveUser.bind(this) }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
UserPicker.propTypes = {
|
||||
'dispatch': PropTypes.func.isRequired,
|
||||
'settings': PropTypes.object.isRequired,
|
||||
'activeUser': PropTypes.object.isRequired,
|
||||
'activeSession': PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'activeUser': state.user,
|
||||
'activeSession': state.session,
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(UserPicker);
|
||||
@ -0,0 +1,36 @@
|
||||
// PasswordField -> Required by Components/UserPanel/Form
|
||||
// --------------------------------------
|
||||
// Simple password input field.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
const PasswordField = (props) => {
|
||||
let classes = ['user-password'];
|
||||
|
||||
if (props.passwordFailed === true) {
|
||||
classes.push('error');
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
id="password-field"
|
||||
type="password"
|
||||
placeholder="*******************"
|
||||
className={ classes.join(' ') }
|
||||
value={ props.password }
|
||||
onInput={ props.handlePasswordInput }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
PasswordField.propTypes = {
|
||||
'password': PropTypes.string.isRequired,
|
||||
'passwordFailed': PropTypes.bool.isRequired,
|
||||
'handlePasswordInput': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default PasswordField;
|
||||
@ -0,0 +1,93 @@
|
||||
// SessionDropdown -> Required by Components/UserPanel/Form
|
||||
// --------------------------------------
|
||||
// Displays session rows as a dropdown to handle
|
||||
// session switching.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import SessionRow from './SessionRow';
|
||||
|
||||
|
||||
export class SessionDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
'dropdownActive': false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
handleClick(sessionKey) {
|
||||
if (sessionKey !== this.props.activeSession) {
|
||||
this.props.setActiveSession(sessionKey);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
'dropdownActive': !this.state.dropdownActive
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleDropdownLeave() {
|
||||
this.setState({
|
||||
'dropdownActive': false
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
// Sort by active, then alphabetical.
|
||||
// Doing this requires using sort in reverse.
|
||||
let rows = (
|
||||
window.lightdm.sessions
|
||||
.sort((a, b) => {
|
||||
return a.name.toUpperCase() > b.name.toUpperCase();
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return (b.key.toLowerCase() === this.props.activeSession.toLowerCase()) ? 1 : -1;
|
||||
})
|
||||
.map((session) => (
|
||||
<SessionRow
|
||||
active={ (this.props.activeSession === session.key) }
|
||||
key={ session.key }
|
||||
session={ session }
|
||||
buttonColor={ this.props.buttonColor }
|
||||
handleClick={ this.handleClick.bind(this) }
|
||||
/>
|
||||
))
|
||||
);
|
||||
|
||||
let classes = ['dropdown', 'user-session'];
|
||||
|
||||
if (this.state.dropdownActive === true) {
|
||||
classes.push('active');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ classes.join(' ') } onMouseLeave={ this.handleDropdownLeave.bind(this) }>
|
||||
{ rows }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SessionDropdown.propTypes = {
|
||||
'activeSession': PropTypes.string.isRequired,
|
||||
'setActiveSession': PropTypes.func.isRequired,
|
||||
'buttonColor': PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'activeSession': state.session.key,
|
||||
'buttonColor': state.settings.style_login_button_color
|
||||
};
|
||||
},
|
||||
null
|
||||
)(SessionDropdown);
|
||||
@ -0,0 +1,42 @@
|
||||
// SessionRow -> Required by Components/UserPanel/SessionDropdown
|
||||
// --------------------------------------
|
||||
// Just a row.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import cxs from "cxs";
|
||||
|
||||
|
||||
const SessionRow = (props) => {
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
props.handleClick(props.session.key);
|
||||
};
|
||||
|
||||
let classes = ['dropdown-item'];
|
||||
|
||||
if (props.active === true) {
|
||||
classes.push('active');
|
||||
|
||||
classes.push(cxs({
|
||||
"background-color": props.buttonColor
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ classes.join(' ') } key={ props.session.key } onClick={ handleClick }>
|
||||
{ props.session.name }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
SessionRow.propTypes = {
|
||||
'active': PropTypes.bool.isRequired,
|
||||
'buttonColor': PropTypes.string.isRequired,
|
||||
'session': PropTypes.object.isRequired,
|
||||
'handleClick': PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
export default SessionRow;
|
||||
@ -0,0 +1,177 @@
|
||||
// UserSwitcher -> Required by Components/UserPanel
|
||||
// --------------------------------------
|
||||
// Handles (poorly) the task of switching between
|
||||
// multiple users on the same system.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const FADE_DURATION = 200;
|
||||
|
||||
|
||||
class UserSwitcher extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
"fadeOut": false,
|
||||
"selectedUser": this.props.activeUser,
|
||||
"selectedUserIndex": window.lightdm.users.indexOf(this.props.activeUser)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
handleBackButton(event) {
|
||||
this.props.setActiveUser(this.state.selectedUser);
|
||||
|
||||
this.setState({
|
||||
"fadeOut": true
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
"fadeOut": false
|
||||
});
|
||||
}, FADE_DURATION);
|
||||
}
|
||||
|
||||
|
||||
handleUserClick(index) {
|
||||
this.setState({
|
||||
"selectedUser": window.lightdm.users[index],
|
||||
"selectedUserIndex": index
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
generateUserList() {
|
||||
let activeIndex = this.state.selectedUserIndex;
|
||||
|
||||
let avatarBackgroundClasses;
|
||||
|
||||
if(this.props.avatarEnabled) {
|
||||
avatarBackgroundClasses = 'avatar-background';
|
||||
} else {
|
||||
avatarBackgroundClasses = 'avatar-background avatar-background-hidden';
|
||||
}
|
||||
|
||||
let avatars = window.lightdm.users.map((user, index) => {
|
||||
let classes = ['avatar-container'];
|
||||
|
||||
if (index === activeIndex) {
|
||||
classes.push('active');
|
||||
}
|
||||
|
||||
if (index === activeIndex - 1) {
|
||||
classes.push('previous');
|
||||
}
|
||||
|
||||
if (index === activeIndex + 1) {
|
||||
classes.push('next');
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={ classes.join(' ') } onClick={ this.handleUserClick.bind(this, index) } key={ user.display_name || user.real_name }>
|
||||
<div className={ avatarBackgroundClasses }>
|
||||
<div className="avatar-mask">
|
||||
<img className="user-avatar" src={ user.image } />
|
||||
</div>
|
||||
</div>
|
||||
<div className="avatar-name">
|
||||
<div className="username">{ user.display_name }</div>
|
||||
<div className="real-name">{ user.real_name }</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
// Very hacky. Add an extra copy of the last element to the beginning of the list
|
||||
// if the first element in the list is currently selected.
|
||||
if (activeIndex === 0) {
|
||||
let user = window.lightdm.users[window.lightdm.users.length - 1];
|
||||
avatars.splice(0, 0,
|
||||
<li className="avatar-container previous" onClick={ this.handleUserClick.bind(this, window.lightdm.users.length - 1) } key="ecopy1">
|
||||
<div className={ avatarBackgroundClasses }>
|
||||
<div className="avatar-mask">
|
||||
<img className="user-avatar" src={ user.image } />
|
||||
</div>
|
||||
</div>
|
||||
<div className="avatar-name">
|
||||
<div className="username">{ user.display_name }</div>
|
||||
<div className="real-name">{ user.real_name }</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
// Very hacky. Add an extra copy of the first element to the end of the list
|
||||
// if the last element in the list is currently selected.
|
||||
if (activeIndex === window.lightdm.users.length - 1) {
|
||||
let user = window.lightdm.users[0];
|
||||
avatars.push(
|
||||
<li className="avatar-container next" onClick={ this.handleUserClick.bind(this, 0) } key="ecopy2">
|
||||
<div className={ avatarBackgroundClasses }>
|
||||
<div className="avatar-mask">
|
||||
<img className="user-avatar" src={ user.image } />
|
||||
</div>
|
||||
</div>
|
||||
<div className="avatar-name">
|
||||
<div className="username">{ user.display_name }</div>
|
||||
<div className="real-name">{ user.real_name }</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="avatar-slider">
|
||||
{ avatars }
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let classes = ['login-panel-switcher'];
|
||||
|
||||
let userList = this.generateUserList();
|
||||
let userCount = window.lightdm.users.length;
|
||||
|
||||
if (this.props.active === true) {
|
||||
classes.push('active');
|
||||
} else if (this.state.fadeOut === true) {
|
||||
classes.push('fadeout');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ classes.join(' ') }>
|
||||
<div className="header">User <em>{ this.state.selectedUserIndex + 1 }</em> of <em>{ userCount }</em></div>
|
||||
{ userList }
|
||||
<div className="bottom" onClick={ this.handleBackButton.bind(this) }>
|
||||
<div className="left">BACK</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
UserSwitcher.propTypes = {
|
||||
'active': PropTypes.bool.isRequired,
|
||||
'activeUser': PropTypes.object.isRequired,
|
||||
'setActiveUser': PropTypes.func.isRequired,
|
||||
'avatarEnabled': PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'activeUser': state.user,
|
||||
'avatarEnabled': state.settings.avatar_background_enabled
|
||||
};
|
||||
},
|
||||
null
|
||||
)(UserSwitcher);
|
||||
@ -0,0 +1,27 @@
|
||||
// UserSwitchButton -> Required by Components/UserPanel
|
||||
// --------------------------------------
|
||||
// Toggles the UserSwitcher.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
export const UserSwitchButton = ({ handleSwitcherClick }) => {
|
||||
let classes = ['left'];
|
||||
|
||||
if (window.lightdm.users.length < 2) {
|
||||
classes.push('disabled');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ classes.join(' ') } onClick={ handleSwitcherClick }>SWITCH USER</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
UserSwitchButton.propTypes = {
|
||||
'handleSwitcherClick': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default UserSwitchButton;
|
||||
@ -0,0 +1,210 @@
|
||||
const Glass = {
|
||||
'avatar_enabled': false,
|
||||
'avatar_size': '200px',
|
||||
'avatar_shape': 'circle',
|
||||
'date_enabled': true,
|
||||
'date_format': '<em>%A</em>, the <em>%o</em> of <em>%B</em>',
|
||||
'time_enabled': true,
|
||||
'time_format': '%H:%M',
|
||||
'hostname_enabled': true,
|
||||
'command_shutdown_enabled': true,
|
||||
'command_reboot_enabled': true,
|
||||
'command_hibernate_enabled': true,
|
||||
'command_sleep_enabled': true,
|
||||
'command_icons_enabled': true,
|
||||
'command_text_align': 'left',
|
||||
'style_command_background_color': 'hsla(0, 2%, 98%, 0.29)',
|
||||
'style_command_icon_color': 'hsl(182, 79%, 58%)',
|
||||
'style_command_text_color': 'hsla(0, 0%, 35%, 0.9)',
|
||||
'style_login_border_color': 'hsla(0, 0%, 0%, 0)',
|
||||
'style_login_border_enabled': false,
|
||||
'style_login_button_color': 'hsla(16, 96%, 11%, 0.88)',
|
||||
'style_login_gradient_top_color': 'hsla(0, 0%, 100%, 0)',
|
||||
'style_login_gradient_bottom_color': 'hsla(0, 0%, 100%, 0.32)',
|
||||
'style_login_username_color': 'hsla(0, 100%, 100%, 1)',
|
||||
'window_border_radius': '4px',
|
||||
'window_font_size': '1em'
|
||||
};
|
||||
|
||||
const Default = {
|
||||
'avatar_enabled': false,
|
||||
'avatar_size': '200px',
|
||||
'avatar_shape': 'circle',
|
||||
'date_enabled': true,
|
||||
'date_format': '<em>%A</em>, the <em>%o</em> of <em>%B</em>',
|
||||
'time_enabled': true,
|
||||
'time_format': '%H:%M',
|
||||
'hostname_enabled': true,
|
||||
'command_shutdown_enabled': true,
|
||||
'command_reboot_enabled': true,
|
||||
'command_hibernate_enabled': true,
|
||||
'command_sleep_enabled': true,
|
||||
'command_icons_enabled': true,
|
||||
'command_text_align': 'left',
|
||||
'style_command_background_color': 'hsla(0, 0%, 22%, 1)',
|
||||
'style_command_icon_color': 'hsla(349, 98%, 65%, 1)',
|
||||
'style_command_text_color': 'hsla(0, 100%, 100%, 1)',
|
||||
'style_login_border_color': 'hsla(0, 100%, 100%, 0.1)',
|
||||
'style_login_border_enabled': true,
|
||||
'style_login_button_color': 'hsla(0, 100%, 100%, 0.22)',
|
||||
'style_login_gradient_top_color': 'hsla(18, 100%, 61%, 0.66)',
|
||||
'style_login_gradient_bottom_color': 'hsla(339, 94%, 64%, 1)',
|
||||
'style_login_username_color': 'hsla(0, 100%, 100%, 1)',
|
||||
'window_border_radius': '4px',
|
||||
'window_font_size': '1em'
|
||||
};
|
||||
|
||||
const Arch = {
|
||||
'avatar_enabled': true,
|
||||
'avatar_size': '200px',
|
||||
'avatar_shape': 'circle',
|
||||
'date_enabled': true,
|
||||
'date_format': '<em>%A</em>, the <em>%o</em> of <em>%B</em>',
|
||||
'time_enabled': true,
|
||||
'time_format': '%H:%M',
|
||||
'hostname_enabled': true,
|
||||
'command_shutdown_enabled': true,
|
||||
'command_reboot_enabled': true,
|
||||
'command_hibernate_enabled': true,
|
||||
'command_sleep_enabled': true,
|
||||
'command_icons_enabled': true,
|
||||
'command_text_align': 'left',
|
||||
'style_command_background_color': 'hsl(201, 48%, 15%)',
|
||||
'style_command_icon_color': 'hsl(199, 66%, 65%)',
|
||||
'style_command_text_color': 'hsl(0, 100%, 96%)',
|
||||
'style_login_border_color': 'hsla(0, 100%, 50%, 0.1)',
|
||||
'style_login_border_enabled': false,
|
||||
'style_login_button_color': 'hsla(0, 0%, 0%, 0)',
|
||||
'style_login_gradient_top_color': 'hsl(32, 76%, 76%)',
|
||||
'style_login_gradient_bottom_color': 'hsl(193, 80%, 71%)',
|
||||
'style_login_username_color': 'hsla(0, 100%, 100%, 1)',
|
||||
'window_border_radius': '4px',
|
||||
'window_font_size': '1em'
|
||||
};
|
||||
|
||||
const Capitan = {
|
||||
avatar_enabled: true,
|
||||
avatar_size: '200px',
|
||||
avatar_shape: 'circle',
|
||||
date_enabled: true,
|
||||
date_format: '<em>%A</em>, the <em>%o</em> of <em>%B</em>',
|
||||
time_enabled: true,
|
||||
time_format: '%H:%M',
|
||||
hostname_enabled: true,
|
||||
command_shutdown_enabled: true,
|
||||
command_reboot_enabled: true,
|
||||
command_hibernate_enabled: true,
|
||||
command_sleep_enabled: true,
|
||||
command_icons_enabled: false,
|
||||
command_text_align: 'center',
|
||||
style_command_background_color: 'hsla(236, 4%, 12%, 0.8)',
|
||||
style_command_icon_color: 'hsl(0, 0%, 0%)',
|
||||
style_command_text_color: 'hsl(0, 100%, 96%)',
|
||||
style_login_border_color: 'hsla(0, 100%, 50%, 0.1)',
|
||||
style_login_border_enabled: false,
|
||||
style_login_button_color: 'hsl(0, 0%, 0%)',
|
||||
style_login_gradient_top_color: 'hsla(0, 76%, 76%, 0.94)',
|
||||
style_login_gradient_bottom_color: 'hsl(193, 80%, 71%)',
|
||||
style_login_username_color: 'hsla(0, 100%, 100%, 1)',
|
||||
window_border_radius: '4px',
|
||||
window_font_size: '1em'
|
||||
};
|
||||
|
||||
|
||||
const Ember = {
|
||||
avatar_enabled: false,
|
||||
avatar_size: '200px',
|
||||
avatar_shape: 'circle',
|
||||
date_enabled: true,
|
||||
date_format: '<em>%A</em>, the <em>%o</em> of <em>%B</em>',
|
||||
time_enabled: true,
|
||||
time_format: '%H:%M',
|
||||
hostname_enabled: true,
|
||||
command_shutdown_enabled: true,
|
||||
command_reboot_enabled: true,
|
||||
command_hibernate_enabled: true,
|
||||
command_sleep_enabled: true,
|
||||
command_icons_enabled: true,
|
||||
command_text_align: 'left',
|
||||
style_command_background_color: 'hsl(0, 80%, 26%)',
|
||||
style_command_icon_color: 'hsl(13, 100%, 53%)',
|
||||
style_command_text_color: 'hsl(0, 0%, 100%)',
|
||||
style_login_border_color: 'hsla(46, 82%, 48%, 0.24)',
|
||||
style_login_border_enabled: true,
|
||||
style_login_button_color: 'hsla(0, 100%, 85%, 0.22)',
|
||||
style_login_gradient_top_color: 'hsl(14, 100%, 53%)',
|
||||
style_login_gradient_bottom_color: 'hsl(35, 95%, 56%)',
|
||||
style_login_username_color: 'hsla(0, 100%, 100%, 1)',
|
||||
window_border_radius: '4px',
|
||||
window_font_size: '1em'
|
||||
};
|
||||
|
||||
const MaterialPink = {
|
||||
avatar_enabled: true,
|
||||
avatar_size: '200px',
|
||||
avatar_shape: 'circle',
|
||||
font_scale: '1',
|
||||
date_enabled: true,
|
||||
date_format: '<em>%A</em>, the <em>%o</em> of <em>%B</em>',
|
||||
time_enabled: true,
|
||||
time_format: '%H:%M',
|
||||
hostname_enabled: true,
|
||||
command_shutdown_enabled: true,
|
||||
command_reboot_enabled: true,
|
||||
command_hibernate_enabled: true,
|
||||
command_sleep_enabled: true,
|
||||
command_icons_enabled: true,
|
||||
command_text_align: 'left',
|
||||
style_command_background_color: 'hsl(336, 78%, 43%)',
|
||||
style_command_icon_color: 'hsl(340, 100%, 63%)',
|
||||
style_command_text_color: 'hsl(0, 0%, 100%)',
|
||||
style_login_border_color: 'hsla(0, 0%, 68%, 0.1)',
|
||||
style_login_border_enabled: true,
|
||||
style_login_button_color: 'hsla(0, 0%, 0%, 0.49)',
|
||||
style_login_gradient_top_color: 'hsl(340, 82%, 52%)',
|
||||
style_login_gradient_bottom_color: 'hsl(340, 82%, 52%)',
|
||||
style_login_username_color: 'hsla(0, 100%, 100%, 1)',
|
||||
window_border_radius: '4px',
|
||||
window_font_size: '1em'
|
||||
};
|
||||
|
||||
const MaterialGreen = {
|
||||
avatar_enabled: true,
|
||||
avatar_size: '200px',
|
||||
avatar_shape: 'circle',
|
||||
font_scale: '1',
|
||||
date_enabled: true,
|
||||
date_format: '<em>%A</em>, the <em>%o</em> of <em>%B</em>',
|
||||
time_enabled: true,
|
||||
time_format: '%H:%M',
|
||||
hostname_enabled: true,
|
||||
command_shutdown_enabled: true,
|
||||
command_reboot_enabled: true,
|
||||
command_hibernate_enabled: true,
|
||||
command_sleep_enabled: true,
|
||||
command_icons_enabled: true,
|
||||
command_text_align: 'left',
|
||||
style_command_background_color: 'hsl(123, 43%, 39%)',
|
||||
style_command_icon_color: 'hsl(88, 50%, 53%)',
|
||||
style_command_text_color: 'hsl(0, 0%, 100%)',
|
||||
style_login_border_color: 'hsla(0, 0%, 68%, 0.1)',
|
||||
style_login_border_enabled: true,
|
||||
style_login_button_color: 'hsla(0, 0%, 0%, 0.49)',
|
||||
style_login_gradient_top_color: 'hsl(122, 39%, 49%)',
|
||||
style_login_gradient_bottom_color: 'hsl(122, 39%, 49%)',
|
||||
style_login_username_color: 'hsla(0, 100%, 100%, 1)',
|
||||
window_border_radius: '4px',
|
||||
window_font_size: '1em'
|
||||
};
|
||||
|
||||
export const DefaultThemes = {
|
||||
Arch,
|
||||
Default,
|
||||
'El Capitan': Capitan,
|
||||
Glass,
|
||||
Ember,
|
||||
'Material Green': MaterialGreen,
|
||||
'Material Pink': MaterialPink
|
||||
};
|
||||
|
||||
export default DefaultThemes;
|
||||
@ -0,0 +1,182 @@
|
||||
// Settings -> Required by Main
|
||||
// --------------------------------------
|
||||
// Handles greeter configuration.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Draggable from 'draggable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import SectionGeneral from './sections/General';
|
||||
import SectionStyle from './sections/Style';
|
||||
import SectionThemes from './sections/Themes';
|
||||
import SaveDialogue from './SaveDialogue';
|
||||
|
||||
import { setPageZoom } from 'Utils/Utils';
|
||||
|
||||
|
||||
const SETTINGS_HEIGHT = 300;
|
||||
const SETTINGS_WIDTH = 600;
|
||||
|
||||
|
||||
class Settings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
"active": this.props.settings.active,
|
||||
"selectedCategory": 'general',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
let draggable = new Draggable(document.getElementById("settings"), {
|
||||
"handle": this.handle
|
||||
});
|
||||
|
||||
let centerX = ((window.innerWidth - SETTINGS_WIDTH) / 2);
|
||||
let centerY = ((window.innerHeight - SETTINGS_HEIGHT) / 2);
|
||||
|
||||
draggable.set(centerX, centerY);
|
||||
|
||||
// Set default zoom
|
||||
let defaultZoom = this.props.settings.page_zoom;
|
||||
setPageZoom(defaultZoom);
|
||||
}
|
||||
|
||||
|
||||
handleCategoryClick(category, e) {
|
||||
this.setState({
|
||||
"selectedCategory": category.toLowerCase()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleSettingsBinary(name) {
|
||||
this.props.dispatch({
|
||||
"type": 'SETTINGS_TOGGLE_VALUE',
|
||||
"name": name
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleSettingsClose() {
|
||||
this.props.dispatch({
|
||||
"type": 'SETTINGS_TOGGLE_ACTIVE'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleSettingsMinimize() {
|
||||
this.props.dispatch({
|
||||
"type": 'SETTINGS_WINDOW_MINIMIZE'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleSettingsText(name, event) {
|
||||
let value;
|
||||
|
||||
try {
|
||||
value = event.target.value;
|
||||
} catch (err) {
|
||||
value = event;
|
||||
}
|
||||
|
||||
this.props.dispatch({
|
||||
"type": 'SETTINGS_SET_VALUE',
|
||||
"name": name,
|
||||
"value": value
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
generateCategories() {
|
||||
let categories = [
|
||||
'General',
|
||||
'Style',
|
||||
'Themes'
|
||||
];
|
||||
|
||||
let listItems = categories.map((category) => {
|
||||
let classes = [];
|
||||
|
||||
if (category.toLowerCase() === this.state.selectedCategory) {
|
||||
classes.push('active');
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={ category } className={ classes.join(' ') } onClick={ this.handleCategoryClick.bind(this, category) }>
|
||||
{ category }
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{ listItems }
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
generateSection(_category) {
|
||||
let category = _category.toLowerCase();
|
||||
let componentProps = {
|
||||
"settingsToggleBinary": this.handleSettingsBinary.bind(this),
|
||||
"settingsSetValue": this.handleSettingsText.bind(this)
|
||||
};
|
||||
|
||||
if (category === "general") {
|
||||
return (<SectionGeneral { ...componentProps } />);
|
||||
} else if (category === "style") {
|
||||
return (<SectionStyle { ...componentProps } />);
|
||||
} else if (category === "themes") {
|
||||
return (<SectionThemes { ...componentProps } />);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let categories = this.generateCategories();
|
||||
let section = this.generateSection(this.state.selectedCategory);
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div>
|
||||
<div className="settings-handle" ref={ (node) => this.handle = node }>
|
||||
<ul>
|
||||
<li className="settings-minimize" onClick={ this.handleSettingsMinimize.bind(this) }>−</li>
|
||||
<li className="settings-close" onClick={ this.handleSettingsClose.bind(this) }>×</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="settings-categories">
|
||||
{ categories }
|
||||
</div>
|
||||
<div className="settings-section">
|
||||
{ section }
|
||||
<SaveDialogue />
|
||||
</div>
|
||||
</div>,
|
||||
document.getElementById("settings")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Settings.propTypes = {
|
||||
'dispatch': PropTypes.func.isRequired,
|
||||
'settings': PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(Settings);
|
||||
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
const rejectSettings = (props) => {
|
||||
props.dispatch({
|
||||
'type': "SETTINGS_REJECT"
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const saveSettings = (props) => {
|
||||
props.dispatch({
|
||||
'type': "SETTINGS_SAVE"
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const SaveDialogue = (props) => {
|
||||
return (
|
||||
<div className="save-dialogue">
|
||||
<button className="settings-reject" onClick={ rejectSettings.bind(this, props) } >revert</button>
|
||||
<button className="settings-save" onClick={ saveSettings.bind(this, props) } >save</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
SaveDialogue.propTypes = {
|
||||
'dispatch': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {};
|
||||
},
|
||||
null
|
||||
)(SaveDialogue);
|
||||
@ -0,0 +1,36 @@
|
||||
// FormCheckbox -> Required by Settings/Settings*
|
||||
// --------------------------------------
|
||||
// Provides a basic binary form checkbox.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
export const Checkbox = ({ name, value, boundFunction }) => {
|
||||
let elementID = `option-${ name.replace(" ", "-")}`;
|
||||
|
||||
return (
|
||||
<li className="settings-item">
|
||||
<input
|
||||
id={ elementID }
|
||||
type="checkbox"
|
||||
checked={ value }
|
||||
onChange={ boundFunction }
|
||||
/>
|
||||
<label htmlFor={ elementID }>
|
||||
{ name }
|
||||
<div className="fake-checkbox" onChange={ boundFunction }></div>
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Checkbox.propTypes = {
|
||||
'name': PropTypes.string.isRequired,
|
||||
'value': PropTypes.bool.isRequired,
|
||||
'boundFunction': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default Checkbox;
|
||||
@ -0,0 +1,83 @@
|
||||
// FormColorPicker -> Required by Settings/Settings*
|
||||
// --------------------------------------
|
||||
// Wraps the jsColorPicker lib to provide a color picker.
|
||||
|
||||
import React from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ChromePicker } from 'react-color';
|
||||
|
||||
|
||||
export class ColorPicker extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
'active': false,
|
||||
'color': tinycolor(props.value).toHsl()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
handleChange(color, event) {
|
||||
let colorString = tinycolor(color[color.source]).toHslString();
|
||||
|
||||
this.setState({
|
||||
'color': color[color.source]
|
||||
});
|
||||
|
||||
this.props.boundFunction(colorString);
|
||||
}
|
||||
|
||||
|
||||
handleClose(e) {
|
||||
this.setState({ 'active': false });
|
||||
}
|
||||
|
||||
|
||||
handleOpen(e) {
|
||||
this.setState({ 'active': true });
|
||||
}
|
||||
|
||||
|
||||
render () {
|
||||
let elementID = `option-${ this.props.name.replace(" ", "-")}`;
|
||||
let swatchContainerClasses = ['swatch-container'];
|
||||
let colorPicker = false;
|
||||
|
||||
if (this.state.active === true) {
|
||||
swatchContainerClasses.push("active");
|
||||
|
||||
colorPicker = (
|
||||
<ChromePicker color={ this.state.color } onChange={ this.handleChange.bind(this) } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="settings-item settings-color">
|
||||
<label htmlFor={ elementID } title={ this.props.name }>{ this.props.name }</label>
|
||||
<div id={ elementID } className={ swatchContainerClasses.join(" ") }>
|
||||
<div className="swatch" onClick={ this.handleOpen.bind(this) }>
|
||||
<div className="swatch-fg" style={{ 'backgroundColor': this.props.value }} />
|
||||
<div className="swatch-bg swatch-bg-black" />
|
||||
<div className="swatch-bg swatch-bg-gray" />
|
||||
<div className="swatch-bg swatch-bg-white" />
|
||||
</div>
|
||||
{ colorPicker }
|
||||
<div className="colorpicker-background" onClick={ this.handleClose.bind(this) }/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ColorPicker.propTypes = {
|
||||
'value': PropTypes.string.isRequired,
|
||||
'name': PropTypes.string.isRequired,
|
||||
'boundFunction': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default ColorPicker;
|
||||
@ -0,0 +1,46 @@
|
||||
// FormDropdown -> Required by Settings/Settings*
|
||||
// --------------------------------------
|
||||
// Provides a basic form dropdown.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
export const DropdownOption = (option) => {
|
||||
let name = option.name || option;
|
||||
let value = option.value || option;
|
||||
|
||||
return (
|
||||
<option key={ `key-${name}-${value}` } value={ value }>{ name }</option>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const Dropdown = ({ name, value, options, boundFunction }) => {
|
||||
let elementID = `option-${ name.replace(" ", "-") }`;
|
||||
let items = options.map((option) => DropdownOption(option));
|
||||
|
||||
return (
|
||||
<li className="settings-item">
|
||||
<If condition={ name !== undefined }>
|
||||
<label htmlFor={ elementID }>{ name }</label>
|
||||
</If>
|
||||
<select id={ elementID } onChange={ boundFunction } value={ value }>
|
||||
{ items }
|
||||
</select>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Dropdown.propTypes = {
|
||||
'name': PropTypes.string,
|
||||
'value': PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
'options': PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([PropTypes.object, PropTypes.string, PropTypes.number])
|
||||
).isRequired,
|
||||
'boundFunction': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default Dropdown;
|
||||
@ -0,0 +1,32 @@
|
||||
// FormTextField -> Required by Settings/Settings*
|
||||
// --------------------------------------
|
||||
// Provides a basic binary form checkbox.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export const TextField = ({ name, value, boundFunction }) => {
|
||||
let elementID = `option-${ name.replace(" ", "-")}`;
|
||||
|
||||
return (
|
||||
<li className="settings-item">
|
||||
<label htmlFor={ elementID }>{ name }</label>
|
||||
<input
|
||||
id={ elementID }
|
||||
type="text"
|
||||
onInput={ boundFunction }
|
||||
defaultValue={ value }
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
TextField.propTypes = {
|
||||
'name': PropTypes.string.isRequired,
|
||||
'value': PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
'boundFunction': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default TextField;
|
||||
@ -0,0 +1,162 @@
|
||||
// SettingsGeneral -> Required by Components/Settings
|
||||
// --------------------------------------
|
||||
// Basic distro / visibility / date & time formatting settings.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import * as FileOperations from "Logic/FileOperations";
|
||||
import TextField from "../inputs/TextField";
|
||||
import Checkbox from "../inputs/Checkbox";
|
||||
|
||||
|
||||
const onLogoChange = (props, e) => {
|
||||
props.dispatch({
|
||||
"type": 'SETTINGS_LOGO_CHANGE',
|
||||
"path": e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const LogoChooser = (props) => {
|
||||
let logos = FileOperations.getLogos();
|
||||
let activeLogo = props.settings.distro;
|
||||
|
||||
let items = logos.map((e) => {
|
||||
let [path, fileName] = e;
|
||||
|
||||
return (
|
||||
<option key={ fileName } value={ path }>{ fileName.split(".")[0] }</option>
|
||||
);
|
||||
});
|
||||
|
||||
let selectedItem = logos.filter((e) => (e[0] === activeLogo));
|
||||
selectedItem = selectedItem[0] || [""];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="preview-logo">
|
||||
<img src={ selectedItem[0] } />
|
||||
</div>
|
||||
<select onChange={ onLogoChange.bind(this, props) } value={ activeLogo }>
|
||||
{ items }
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
LogoChooser.propTypes = {
|
||||
'settings': PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
||||
export const GeneralSection = (props) => {
|
||||
const settings = props.settings;
|
||||
|
||||
return (
|
||||
<div className="settings-general">
|
||||
<div className="left">
|
||||
{ LogoChooser(props) }
|
||||
</div>
|
||||
<div className="right">
|
||||
<ul>
|
||||
<h4>User Functionality</h4>
|
||||
<hr />
|
||||
<Checkbox
|
||||
name={ "Show User Switcher" }
|
||||
value={ settings.user_switcher_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'user_switcher_enabled') }
|
||||
/>
|
||||
|
||||
<h4>Date & Time</h4>
|
||||
<hr />
|
||||
<Checkbox
|
||||
name={ "Date Enabled" }
|
||||
value={ settings.date_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'date_enabled') }
|
||||
/>
|
||||
<TextField
|
||||
name={ "Date Format" }
|
||||
value={ settings.date_format }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'date_format') }
|
||||
/>
|
||||
<Checkbox
|
||||
name={ "Time Enabled" }
|
||||
value={ settings.time_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'time_enabled') }
|
||||
/>
|
||||
<TextField
|
||||
name={ "Time Format" }
|
||||
value={ settings.time_format }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'time_format') }
|
||||
/>
|
||||
|
||||
<h4>Command Visibility</h4>
|
||||
<hr />
|
||||
<Checkbox
|
||||
name={ "Shutdown Enabled" }
|
||||
value={ settings.command_shutdown_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'command_shutdown_enabled') }
|
||||
/>
|
||||
<Checkbox
|
||||
name={ "Reboot Enabled" }
|
||||
value={ settings.command_reboot_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'command_reboot_enabled') }
|
||||
/>
|
||||
<Checkbox
|
||||
name={ "Hibernate Enabled" }
|
||||
value={ settings.command_hibernate_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'command_hibernate_enabled') }
|
||||
/>
|
||||
<Checkbox
|
||||
name={ "Sleep Enabled" }
|
||||
value={ settings.command_sleep_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'command_sleep_enabled') }
|
||||
/>
|
||||
|
||||
<h4>Avatar Visibility</h4>
|
||||
<hr />
|
||||
<Checkbox
|
||||
name={ "Avatar Enabled" }
|
||||
value={ settings.avatar_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'avatar_enabled') }
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
name={ "Background Enabled" }
|
||||
value= { settings.avatar_background_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'avatar_background_enabled') }
|
||||
/>
|
||||
|
||||
<h4>Hostname Visibility</h4>
|
||||
<hr />
|
||||
<Checkbox
|
||||
name={ "Hostname Enabled" }
|
||||
value={ settings.hostname_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'hostname_enabled') }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
GeneralSection.propTypes = {
|
||||
'settings': PropTypes.object.isRequired,
|
||||
'settingsSetValue': PropTypes.func.isRequired,
|
||||
'settingsToggleBinary': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(GeneralSection);
|
||||
@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ColorPicker from '../inputs/ColorPicker';
|
||||
import TextField from '../inputs/TextField';
|
||||
import Checkbox from '../inputs/Checkbox';
|
||||
import Dropdown from '../inputs/Dropdown';
|
||||
|
||||
|
||||
export const StyleSection = (props) => {
|
||||
const settings = props.settings;
|
||||
|
||||
return (
|
||||
<div className="settings-style">
|
||||
<div className="left">
|
||||
<ul>
|
||||
<h4>Window Appearance</h4>
|
||||
<hr />
|
||||
<TextField
|
||||
name={ "Border Radius" }
|
||||
value={ settings.window_border_radius }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'window_border_radius') }
|
||||
/>
|
||||
<TextField
|
||||
name={ "Font-Size" }
|
||||
value={ settings.window_font_size }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'window_font_size') }
|
||||
/>
|
||||
<TextField
|
||||
name={ "DPI Zoom" }
|
||||
value={ settings.page_zoom }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'page_zoom') }
|
||||
/>
|
||||
|
||||
<h4>Eye Candy</h4>
|
||||
<hr />
|
||||
<Checkbox
|
||||
name={ "Experimental Stars" }
|
||||
value={ settings.experimental_stars_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'experimental_stars_enabled') }
|
||||
/>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div className="right">
|
||||
<ul>
|
||||
<h4>Command Panel</h4>
|
||||
<hr />
|
||||
<Checkbox
|
||||
name={ "Icons Enabled" }
|
||||
value={ settings.command_icons_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'command_icons_enabled') }
|
||||
/>
|
||||
<Dropdown
|
||||
name={ "Text Align" }
|
||||
value={ settings.command_text_align }
|
||||
options={ ['left', 'center', 'right'] }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'command_text_align') }
|
||||
/>
|
||||
<div className="color-group">
|
||||
<ColorPicker
|
||||
name={ "Background" }
|
||||
value={ settings.style_command_background_color }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'style_command_background_color') }
|
||||
/>
|
||||
<ColorPicker
|
||||
name={ "Icon Color" }
|
||||
value={ settings.style_command_icon_color }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'style_command_icon_color') }
|
||||
/>
|
||||
<ColorPicker
|
||||
name={ "Text Color" }
|
||||
value={ settings.style_command_text_color }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'style_command_text_color') }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h4>Login Panel</h4>
|
||||
<hr />
|
||||
<Checkbox
|
||||
name={ "Border Enabled" }
|
||||
value={ settings.style_login_border_enabled }
|
||||
boundFunction={ props.settingsToggleBinary.bind(this, 'style_login_border_enabled') }
|
||||
/>
|
||||
<div className="color-group">
|
||||
<ColorPicker
|
||||
name={ "Border Color" }
|
||||
value={ settings.style_login_border_color }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'style_login_border_color') }
|
||||
/>
|
||||
<ColorPicker
|
||||
name={ "Gradient-Top" }
|
||||
value={ settings.style_login_gradient_top_color }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'style_login_gradient_top_color') }
|
||||
/>
|
||||
<ColorPicker
|
||||
name={ "Gradient-Bottom" }
|
||||
value={ settings.style_login_gradient_bottom_color }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'style_login_gradient_bottom_color') }
|
||||
/>
|
||||
<ColorPicker
|
||||
name={ "Button Color" }
|
||||
value={ settings.style_login_button_color }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'style_login_button_color') }
|
||||
/>
|
||||
<ColorPicker
|
||||
name={ "Username" }
|
||||
value={ settings.style_login_username_color }
|
||||
boundFunction={ props.settingsSetValue.bind(this, 'style_login_username_color') }
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
StyleSection.propTypes = {
|
||||
'settings': PropTypes.object.isRequired,
|
||||
'settingsSetValue': PropTypes.func.isRequired,
|
||||
'settingsToggleBinary': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(StyleSection);
|
||||
@ -0,0 +1,162 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import * as Settings from 'Logic/Settings';
|
||||
import DefaultThemes from '../DefaultThemes';
|
||||
|
||||
|
||||
class Theme extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
|
||||
getColors() {
|
||||
let colors = [];
|
||||
|
||||
for (let setting of Object.keys(this.props.theme)) {
|
||||
if (setting.startsWith('style') && setting.indexOf('color') !== -1) {
|
||||
colors.push([setting, this.props.theme[setting]]);
|
||||
}
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let colorItems = this.getColors().map(([name, color]) =>
|
||||
<li
|
||||
key={ name }
|
||||
className="theme-color-block"
|
||||
style={ { 'backgroundColor': color } }
|
||||
alt={ color }
|
||||
title={ color }
|
||||
>
|
||||
|
||||
</li>
|
||||
);
|
||||
|
||||
let isDefaultTheme = !(Object.keys(DefaultThemes).indexOf(this.props.name) !== -1);
|
||||
|
||||
return (
|
||||
<div className="theme">
|
||||
<div className="upper">
|
||||
<h5 className="theme-name">{ this.props.name}</h5>
|
||||
<button onClick={ this.props.loadTheme.bind(this, this.props.name, this.props.theme) }>preview</button>
|
||||
<If condition={ isDefaultTheme }>
|
||||
<button className="delete" onClick={ this.props.deleteTheme.bind(this, this.props.name) }>delete</button>
|
||||
</If>
|
||||
</div>
|
||||
<ul>
|
||||
{ colorItems }
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Theme.propTypes = {
|
||||
'name': PropTypes.string.isRequired,
|
||||
'theme': PropTypes.object.isRequired,
|
||||
'loadTheme': PropTypes.func.isRequired,
|
||||
'deleteTheme': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export class SettingsThemes extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
'themes': { ...Settings.getUserThemes(), ...DefaultThemes }
|
||||
};
|
||||
|
||||
this.nodes = {};
|
||||
}
|
||||
|
||||
handleDeleteTheme(themeName) {
|
||||
Settings.deleteTheme(themeName);
|
||||
|
||||
this.setState({
|
||||
'themes': { ...Settings.getUserThemes(), ...DefaultThemes }
|
||||
});
|
||||
|
||||
window.notifications.generate(`Theme has been deleted!`, "success");
|
||||
}
|
||||
|
||||
|
||||
handleLoadTheme(themeName, theme) {
|
||||
this.props.dispatch({
|
||||
'type': 'SETTINGS_APPLY_THEME',
|
||||
'name': themeName,
|
||||
'theme': theme
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleSaveTheme(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let themeName = this.nodes.themeName.value;
|
||||
|
||||
Settings.saveTheme(themeName, this.props.settings);
|
||||
|
||||
this.setState({
|
||||
'themes': { ...Settings.getUserThemes(), ...DefaultThemes }
|
||||
});
|
||||
|
||||
window.notifications.generate(`Your theme has been saved.`, "success");
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let themes = this.state.themes;
|
||||
let themeItems = Object.keys(themes).map(themeName =>
|
||||
<Theme
|
||||
key={ themeName }
|
||||
name={ themeName }
|
||||
theme={ themes[themeName] }
|
||||
loadTheme={ this.handleLoadTheme.bind(this) }
|
||||
deleteTheme={ this.handleDeleteTheme.bind(this) }
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="settings-themes">
|
||||
<div className="theme-saver">
|
||||
<p>Save current settings as a theme?</p>
|
||||
<input type="text" name="theme-name" defaultValue="" placeholder="Theme Name" ref={ node => this.nodes.themeName = node } />
|
||||
<button className="save-theme" onClick={ this.handleSaveTheme.bind(this) }>
|
||||
save theme
|
||||
</button>
|
||||
</div>
|
||||
<div className="theme-list">
|
||||
{ themeItems }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SettingsThemes.propTypes = {
|
||||
'settings': PropTypes.object.isRequired,
|
||||
'dispatch': PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
'settings': state.settings
|
||||
};
|
||||
},
|
||||
null
|
||||
)(SettingsThemes);
|
||||
@ -0,0 +1,36 @@
|
||||
// SettingsToggler -> Required by Main
|
||||
// --------------------------------------
|
||||
// Handles Settings toggling. Straightforward stuff.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
const toggleSettings = (props) => {
|
||||
props.dispatch({
|
||||
'type': "SETTINGS_TOGGLE_ACTIVE"
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const SettingsToggler = (props) => {
|
||||
let classes = ['settings-toggler'];
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className={ classes.join(' ') }
|
||||
onClick={ toggleSettings.bind(this, props) }
|
||||
>
|
||||
≡
|
||||
</div>,
|
||||
document.getElementById("settings-toggler-mount")
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => { return {}; },
|
||||
null
|
||||
)(SettingsToggler);
|
||||
@ -0,0 +1,66 @@
|
||||
// FileOperations -> Required by Components/WallpaperSwitcher
|
||||
// --------------------------------------
|
||||
// LightDM related file / config fetching.
|
||||
|
||||
export function getWallpaperDirectory() {
|
||||
// Return the test folder when debugging.
|
||||
if (window.__debug === true) {
|
||||
return "src/test/wallpapers/";
|
||||
}
|
||||
|
||||
let wallpapersDirectory = window.config.get_str("branding", "background_images");
|
||||
|
||||
// Do NOT allow the default wallpaper directory to set, as this will prevent the default provided backgrounds from
|
||||
// being used 100% of the time in a stock install.
|
||||
if (wallpapersDirectory == "/usr/share/backgrounds" || wallpapersDirectory == "/usr/share/backgrounds/") {
|
||||
wallpapersDirectory = "/usr/share/lightdm-webkit/themes/lightdm-webkit-theme-aether/src/img/wallpapers/";
|
||||
}
|
||||
|
||||
return wallpapersDirectory;
|
||||
}
|
||||
|
||||
|
||||
export function getWallpapers(directory) {
|
||||
// If we're in test mode, we stick to a static rotation of three default wallpapers.
|
||||
// In production, it is possible that a user will change what wallpapers are available.
|
||||
if (window.__debug === true) {
|
||||
return ['boko.jpg', 'mountains-2.png', 'space-1.jpg'];
|
||||
}
|
||||
|
||||
let wallpapers;
|
||||
|
||||
wallpapers = window.greeterutil.dirlist(directory);
|
||||
wallpapers = wallpapers.map((e) => e.split("/").pop());
|
||||
|
||||
return wallpapers;
|
||||
}
|
||||
|
||||
|
||||
export function getLogos() {
|
||||
// If we're in test mode, just return the default three.
|
||||
if (window.__debug === true) {
|
||||
return [
|
||||
["src/test/logos/archlinux.png", "archlinux.png"],
|
||||
["src/test/logos/antergos.png", "antergos.png"],
|
||||
["src/test/logos/ubuntu.png", "ubuntu.png"]
|
||||
];
|
||||
}
|
||||
|
||||
// Return a tuple of the path and filename for usage in the Settings dialogue.
|
||||
let userLogo = window.config.get_str("branding", "logo");
|
||||
let themeLogos = window.greeterutil.dirlist("/usr/share/lightdm-webkit/themes/lightdm-webkit-theme-aether/src/img/logos/");
|
||||
|
||||
themeLogos.push(userLogo);
|
||||
|
||||
return themeLogos.map((e) => [e, e.split("/").pop()]);
|
||||
}
|
||||
|
||||
|
||||
export function getEnvironments() {
|
||||
return window.lightdm.sessions.map((session) => {
|
||||
return {
|
||||
'name': session.name,
|
||||
'value': session.key
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
// Settings -> Required by Reducers/PrimaryReducer
|
||||
// --------------------------------------
|
||||
// Handles manipulation of greeter settings, and
|
||||
// provides wrapper functions around localstorage.
|
||||
|
||||
export const LOCALSTORAGE_ENABLED = (typeof(Storage) !== "undefined");
|
||||
|
||||
if (!LOCALSTORAGE_ENABLED) {
|
||||
window.notifications.generate("localStorage not supported. Theme unable to function!", 'error');
|
||||
throw("localStorage not supported. Theme unable to function!");
|
||||
}
|
||||
|
||||
|
||||
export function requestSetting(setting, defaultSetting=undefined) {
|
||||
// Always return 'active' as false when initializing.
|
||||
if (setting === 'active') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Continue as usual
|
||||
let result = localStorage.getItem(setting);
|
||||
|
||||
if (result === null || result === undefined) {
|
||||
return defaultSetting;
|
||||
} else {
|
||||
// Cast string values to booleans if necessary.
|
||||
if (result === "true" || result === "false") {
|
||||
return (result === "true") ? true : false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function saveSetting(setting, value=undefined) {
|
||||
localStorage.setItem(setting, value);
|
||||
}
|
||||
|
||||
|
||||
export function getUserThemes() {
|
||||
let themes = localStorage.getItem('themes');
|
||||
|
||||
if (themes === null || themes === undefined) {
|
||||
themes = {};
|
||||
} else {
|
||||
themes = JSON.parse(themes);
|
||||
}
|
||||
|
||||
return themes;
|
||||
}
|
||||
|
||||
|
||||
export function saveTheme(name, settings) {
|
||||
let themes = getUserThemes();
|
||||
themes[name] = settings;
|
||||
|
||||
localStorage.setItem('themes', JSON.stringify(themes));
|
||||
}
|
||||
|
||||
|
||||
export function deleteTheme(name) {
|
||||
let themes = getUserThemes();
|
||||
|
||||
delete themes[name];
|
||||
|
||||
localStorage.setItem('themes', JSON.stringify(themes));
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
// SystemOperations -> Required by Reducers/PrimaryReducer
|
||||
// --------------------------------------
|
||||
// Wraps LightDM system operations, and handles the heavy
|
||||
// lifting of the more complex LightDM requests.
|
||||
|
||||
function executeCommand(message, boundFunction) {
|
||||
window.notifications.generate(message);
|
||||
|
||||
setTimeout(() => {
|
||||
boundFunction();
|
||||
}, 1000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export function handleCommand(command) {
|
||||
// What the hell is this, right?
|
||||
if (command === "shutdown" && window.lightdm.can_shutdown) {
|
||||
return executeCommand("Shutting down", window.lightdm.shutdown);
|
||||
} else if (command === "hibernate" && window.lightdm.can_hibernate) {
|
||||
return executeCommand("Hibernating system.", window.lightdm.hibernate);
|
||||
} else if (command === "reboot" && window.lightdm.can_restart) {
|
||||
return executeCommand("Rebooting system.", window.lightdm.restart);
|
||||
} else if (command === "sleep" && window.lightdm.can_suspend) {
|
||||
return executeCommand("Suspending system.", window.lightdm.suspend);
|
||||
}
|
||||
|
||||
// If we have gotten this far, it's because the command is disabled or doesn't exist.
|
||||
window.notifications.generate(`${ command } is disabled on this system.`, "error");
|
||||
}
|
||||
|
||||
|
||||
export function findInitialUser() {
|
||||
// Are we currently in a lock screen?
|
||||
if (window.lightdm.lock_hint === true) {
|
||||
// Default to the very first logged in user.
|
||||
return window.lightdm.users.filter((user) => user.logged_in)[0];
|
||||
}
|
||||
|
||||
else {
|
||||
if (window.lightdm.select_user_hint !== undefined && window.lightdm.select_user_hint !== null) {
|
||||
return window.lightdm.users.filter((user) => user.username === window.lightdm.select_user_hint)[0];
|
||||
} else {
|
||||
return window.lightdm.users[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function findInitialSession(user) {
|
||||
let userSession = (user === undefined) ? undefined : user.session;
|
||||
|
||||
return (
|
||||
this.findSession(userSession) ||
|
||||
this.findSession(window.lightdm.default_session) ||
|
||||
window.lightdm.sessions[0]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function findSession(sessionName) {
|
||||
if (sessionName === undefined || sessionName === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return window.lightdm.sessions.filter((session) =>
|
||||
(session.name.toLowerCase() === sessionName.toLowerCase()) ||
|
||||
(session.key.toLowerCase() === sessionName.toLowerCase())
|
||||
)[0];
|
||||
}
|
||||
63
lightdm/themes/lightdm-webkit-theme-aether/src/js/Main.jsx
Normal file
@ -0,0 +1,63 @@
|
||||
import 'sass/style.sass';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { createStore } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import LoginWindow from './Components/LoginWindow';
|
||||
import Notifications from './Utils/Notifications';
|
||||
|
||||
import { getDefaultState, PrimaryReducer } from './Reducers/PrimaryReducer';
|
||||
import { addAdditionalSettings } from './Reducers/SettingsReducer';
|
||||
|
||||
|
||||
export default function Main() {
|
||||
let initialState = getDefaultState();
|
||||
initialState = addAdditionalSettings(initialState);
|
||||
|
||||
let store;
|
||||
|
||||
if (window.__REDUX_DEVTOOLS_EXTENSION__) {
|
||||
store = createStore(
|
||||
PrimaryReducer,
|
||||
initialState,
|
||||
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||
);
|
||||
} else {
|
||||
store = createStore(PrimaryReducer, initialState);
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={ store }>
|
||||
<LoginWindow />
|
||||
</Provider>,
|
||||
document.getElementById('login-window-mount')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
window.onload = (e) => {
|
||||
// Add notifications to the global scope for error handling
|
||||
window.notifications = new Notifications();
|
||||
|
||||
let init = () => {
|
||||
Main();
|
||||
document.getElementById("password-field").focus();
|
||||
};
|
||||
|
||||
// Horribly convoluted for necessity because reasons
|
||||
if (window.__debug === false) {
|
||||
if (window.lightdm === undefined) {
|
||||
document.addEventListener('GreeterReady', () => {
|
||||
init();
|
||||
});
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
};
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { SettingsReducer } from "./SettingsReducer";
|
||||
import * as SystemOperations from "../Logic/SystemOperations";
|
||||
|
||||
export function getDefaultState() {
|
||||
return {
|
||||
"info": {
|
||||
"hostname": window.lightdm.hostname,
|
||||
"language": window.lightdm.language
|
||||
},
|
||||
"user": SystemOperations.findInitialUser(),
|
||||
"session": SystemOperations.findInitialSession()
|
||||
};
|
||||
}
|
||||
|
||||
export const PrimaryReducer = (state, action) => {
|
||||
if (action.type.startsWith("SETTINGS")) {
|
||||
return SettingsReducer(state, action);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case "AUTH_SET_ACTIVE_SESSION":
|
||||
var session = action.session;
|
||||
|
||||
if (typeof session === typeof String()) {
|
||||
session = SystemOperations.findSession(session);
|
||||
}
|
||||
|
||||
return { ...state, "session": session };
|
||||
|
||||
case "AUTH_SET_ACTIVE_USER":
|
||||
return { ...state, "user": action.user };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,160 @@
|
||||
/* eslint { no-redeclare: 0 } */
|
||||
import * as Settings from '../Logic/Settings';
|
||||
import { setPageZoom } from '../Utils/Utils';
|
||||
|
||||
export function addAdditionalSettings(state) {
|
||||
// Define our defaults
|
||||
|
||||
let distroDefault = (window.__debug === true) ? "src/test/logos/archlinux.png" : "/usr/share/lightdm-webkit/themes/lightdm-webkit-theme-aether/src/img/logos/archlinux.png";
|
||||
|
||||
let defaults = {
|
||||
"active": false,
|
||||
"minimized": false,
|
||||
"distro": distroDefault,
|
||||
"page_zoom": 1.0,
|
||||
|
||||
"avatar_enabled": true,
|
||||
"avatar_size": "200px",
|
||||
"avatar_shape": "circle",
|
||||
"avatar_background_enabled": true,
|
||||
|
||||
"font_scale": 1.0,
|
||||
|
||||
"date_enabled": true,
|
||||
"date_format": "<em>%A</em>, the <em>%o</em> of <em>%B</em>",
|
||||
|
||||
"experimental_stars_enabled": false,
|
||||
|
||||
"time_enabled": true,
|
||||
"time_format": "%H:%M",
|
||||
|
||||
"hostname_enabled": true,
|
||||
|
||||
"user_switcher_enabled": true,
|
||||
|
||||
"command_shutdown_enabled": true,
|
||||
"command_reboot_enabled": true,
|
||||
"command_hibernate_enabled": true,
|
||||
"command_sleep_enabled": true,
|
||||
"command_icons_enabled": true,
|
||||
"command_text_align": "left",
|
||||
|
||||
"style_command_background_color": "hsla(0, 0%, 22%, 1)",
|
||||
"style_command_icon_color": "hsla(349, 98%, 65%, 1)",
|
||||
"style_command_text_color": "hsla(0, 100%, 100%, 1)",
|
||||
"style_login_border_color": "hsla(0, 100%, 100%, 0.1)",
|
||||
"style_login_border_enabled": true,
|
||||
"style_login_button_color": "hsla(0, 100%, 100%, 0.22)",
|
||||
"style_login_gradient_top_color": "hsla(18, 100%, 61%, 0.66)",
|
||||
"style_login_gradient_bottom_color": "hsla(339, 94%, 64%, 1)",
|
||||
"style_login_username_color": "hsla(0, 100%, 100%, 1)",
|
||||
|
||||
"window_border_radius": "4px",
|
||||
"window_font_size": "1em"
|
||||
};
|
||||
|
||||
let settings = {};
|
||||
|
||||
for (let key of Object.keys(defaults)) {
|
||||
settings[key] = Settings.requestSetting(key, defaults[key]);
|
||||
}
|
||||
|
||||
return { ...state, "settings": settings, "cachedSettings": settings };
|
||||
}
|
||||
|
||||
|
||||
export const SettingsReducer = (state, action) => {
|
||||
switch(action.type) {
|
||||
case 'SETTINGS_LOGO_CHANGE':
|
||||
var newSettings = { ...state.settings, "distro": action.path };
|
||||
|
||||
return { ...state, "settings": newSettings };
|
||||
|
||||
case 'SETTINGS_REJECT':
|
||||
// Restore settings from the 'default' state.
|
||||
var newSettings = { ...state.cachedSettings };
|
||||
|
||||
// Create a notification
|
||||
window.notifications.generate("Reverted to previous settings, no changes saved.", "success");
|
||||
|
||||
// This shouldn't be here. It is, though.
|
||||
setPageZoom(newSettings.page_zoom);
|
||||
|
||||
return { ...state, "settings": newSettings };
|
||||
|
||||
case 'SETTINGS_APPLY_THEME':
|
||||
var newSettings = { ...state.cachedSettings, ...action.theme };
|
||||
|
||||
// Create a notification
|
||||
window.notifications.generate(`Loaded ${ action.name } theme. Remember to save!`, "success");
|
||||
|
||||
// This shouldn't be here. It is, though.
|
||||
setPageZoom(newSettings.page_zoom);
|
||||
|
||||
return { ...state, "settings": newSettings };
|
||||
|
||||
case 'SETTINGS_SAVE':
|
||||
// Cycle to localStorage for persistence.
|
||||
for (let key of Object.keys(state.settings)) {
|
||||
Settings.saveSetting(key, state.settings[key]);
|
||||
}
|
||||
|
||||
// Save our new settings as the 'default' state.
|
||||
var newCache = { ...state.settings };
|
||||
|
||||
// Create a notification
|
||||
window.notifications.generate("Settings saved.", "success");
|
||||
|
||||
return { ...state, "cachedSettings": newCache };
|
||||
|
||||
case 'SETTINGS_SET_VALUE':
|
||||
var newSettings = { ...state.settings };
|
||||
|
||||
newSettings[action.name] = action.value;
|
||||
|
||||
// This shouldn't be here. It is, though.
|
||||
setPageZoom(newSettings.page_zoom);
|
||||
|
||||
return { ...state, "settings": newSettings };
|
||||
|
||||
case 'SETTINGS_TOGGLE_ACTIVE':
|
||||
var newSettings = { ...state.settings, "active": !state.settings.active };
|
||||
|
||||
// This shouldn't be here. It is, though.
|
||||
var el = document.getElementById("settings");
|
||||
|
||||
if (newSettings.active === true) {
|
||||
el.className = el.className.replace(" hidden", "");
|
||||
} else {
|
||||
el.className += " hidden";
|
||||
}
|
||||
|
||||
return { ...state, "settings": newSettings };
|
||||
|
||||
case 'SETTINGS_TOGGLE_VALUE':
|
||||
var newSettings = { ...state.settings };
|
||||
|
||||
newSettings[action.name] = !newSettings[action.name];
|
||||
|
||||
return { ...state, "settings": newSettings };
|
||||
|
||||
case 'SETTINGS_WINDOW_MINIMIZE':
|
||||
// This shouldn't be here. It is, though.
|
||||
var categories = document.querySelectorAll(".settings-categories")[0];
|
||||
var section = document.querySelectorAll(".settings-section")[0];
|
||||
|
||||
// Check if the window is already minimized.
|
||||
if (categories.className.indexOf('minimize') !== -1) {
|
||||
categories.className = categories.className.replace('minimize', '');
|
||||
section.className = section.className.replace('minimize', '');
|
||||
} else {
|
||||
categories.className = categories.className + ' minimize';
|
||||
section.className = section.className + ' minimize';
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
export default class Notifications {
|
||||
constructor() {
|
||||
this.container = document.querySelectorAll('.notifications-container')[0];
|
||||
|
||||
if (window.__debug === true) {
|
||||
this.generate("Hey there!", "success");
|
||||
|
||||
setTimeout(() => {
|
||||
this.generate("TIP: Click the logo to switch wallpapers.");
|
||||
}, 2000);
|
||||
|
||||
setTimeout(() => {
|
||||
this.generate("TIP: Access settings by hovering over the bottom left of your screen!");
|
||||
}, 5 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
generate(message, type) {
|
||||
if (type === undefined) {
|
||||
type = "";
|
||||
}
|
||||
|
||||
let notification = document.createElement('div');
|
||||
notification.className = `notification ${ type }`;
|
||||
notification.innerText = message;
|
||||
this.container.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.className += " fadeout";
|
||||
setTimeout(() => {
|
||||
this.container.removeChild(notification);
|
||||
}, 500);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
export const scale = (start, end, percentage, round) => {
|
||||
if (round === undefined) {
|
||||
round = true;
|
||||
}
|
||||
|
||||
let scaledValue = start + ((end - start) * percentage);
|
||||
if (round) {
|
||||
return Math.round(scaledValue);
|
||||
} else {
|
||||
return scaledValue;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export function randomRange(min, max, decimals=0) {
|
||||
let result = Math.random() * (max - min) + min;
|
||||
|
||||
if (decimals === 0) {
|
||||
result = Math.floor(result);
|
||||
} else {
|
||||
result = Math.floor(result * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export const padZeroes = (i) => {
|
||||
return (i < 10) ? "0" + i : i;
|
||||
};
|
||||
|
||||
|
||||
export const setPageZoom = (value) => {
|
||||
document.getElementById("root").style.zoom = value;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
export const interpolatePoints = (a, b, ratio) => {
|
||||
let x = a.x + (b.x - a.x) * ratio;
|
||||
let y = a.y + (b.y - a.y) * ratio;
|
||||
|
||||
return { x, y };
|
||||
};
|
||||
@ -0,0 +1,88 @@
|
||||
@keyframes drop-in-notifications
|
||||
0%
|
||||
top: -200px
|
||||
100%
|
||||
top: 0px
|
||||
|
||||
@keyframes drop-in-options
|
||||
0%
|
||||
top: -100px
|
||||
opacity: 0
|
||||
100%
|
||||
top: 0px
|
||||
opacity: 1
|
||||
|
||||
@keyframes error-shake
|
||||
0%
|
||||
transform: translateX(0px) scale(1)
|
||||
11%
|
||||
transform: translateX(-10px) scale(1.05)
|
||||
22%
|
||||
transform: translateX(10px) scale(1.05)
|
||||
33%
|
||||
transform: translateX(-10px) scale(1.05)
|
||||
44%
|
||||
transform: translateX(10px) scale(1.05)
|
||||
55%
|
||||
transform: translateX(-10px) scale(1.05)
|
||||
66%
|
||||
transform: translateX(10px) scale(1.05)
|
||||
77%
|
||||
transform: translateX(10px) scale(1.05)
|
||||
88%
|
||||
transform: translateX(10px) scale(1)
|
||||
100%
|
||||
transform: translateX(0px)
|
||||
|
||||
@keyframes login-fade-out
|
||||
0%
|
||||
opacity: 1
|
||||
transform: translateY(0)
|
||||
|
||||
90%
|
||||
transform: translateY(10px)
|
||||
|
||||
100%
|
||||
opacity: 0
|
||||
|
||||
@keyframes login-fade-in
|
||||
0%
|
||||
opacity: 0
|
||||
transform: translateY(5px)
|
||||
|
||||
100%
|
||||
opacity: 1
|
||||
transform: translateY(0px)
|
||||
|
||||
@keyframes switcher-fade-in
|
||||
0%
|
||||
opacity: 0
|
||||
transform: translateY(5px) scale(0.95)
|
||||
|
||||
100%
|
||||
opacity: 1
|
||||
transform: translateY(0px) scale(1)
|
||||
|
||||
@keyframes switcher-fade-out
|
||||
0%
|
||||
opacity: 1
|
||||
transform: translateY(0px) scale(1)
|
||||
|
||||
100%
|
||||
opacity: 0
|
||||
transform: translateY(-10px) scale(0.95)
|
||||
|
||||
@keyframes wallpaper-fade-out
|
||||
0%
|
||||
opacity: 1
|
||||
|
||||
100%
|
||||
opacity: 0
|
||||
|
||||
@keyframes window-fade-in
|
||||
0%
|
||||
opacity: 0
|
||||
transform: scale(0.7)
|
||||
100%
|
||||
opacity: 1
|
||||
transform: scale(1)
|
||||
@ -0,0 +1,16 @@
|
||||
// Images
|
||||
$distro-logo: url("src/img/logos/archlinux.png")
|
||||
|
||||
$avatar-background: url("src/img/avatar-background.png")
|
||||
|
||||
$dropdown-caret: url("src/img/dropdown-caret.png")
|
||||
|
||||
// Text
|
||||
$primary-color: #fff
|
||||
|
||||
// Dimensions, Positioning
|
||||
$panel-padding-x: 35px
|
||||
$panel-padding-y: 15px
|
||||
|
||||
$command-panel-width: 255px
|
||||
$login-panel-width: 595px
|
||||
@ -0,0 +1,23 @@
|
||||
@font-face
|
||||
font-family: 'Open Sans'
|
||||
src: url('src/font/OpenSans-Regular-webfont.woff') format('woff')
|
||||
font-weight: normal
|
||||
font-style: normal
|
||||
|
||||
@font-face
|
||||
font-family: 'Open Sans'
|
||||
src: url('src/font/OpenSans-Bold-webfont.woff') format('woff')
|
||||
font-weight: bold
|
||||
font-style: normal
|
||||
|
||||
@font-face
|
||||
font-family: 'Open Sans'
|
||||
src: url('src/font/OpenSans-Italic-webfont.woff') format('woff')
|
||||
font-weight: normal
|
||||
font-style: italic
|
||||
|
||||
@font-face
|
||||
font-family: 'Open Sans'
|
||||
src: url('src/font/OpenSans-BoldItalic-webfont.woff') format('woff')
|
||||
font-weight: bold
|
||||
font-style: italic
|
||||
@ -0,0 +1,46 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain) */
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video
|
||||
margin: 0
|
||||
padding: 0
|
||||
border: 0
|
||||
font-size: 100%
|
||||
font: inherit
|
||||
vertical-align: baseline
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section
|
||||
display: block
|
||||
|
||||
body
|
||||
line-height: 1
|
||||
|
||||
ol, ul
|
||||
list-style: none
|
||||
|
||||
blockquote, q
|
||||
quotes: none
|
||||
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after
|
||||
content: ''
|
||||
content: none
|
||||
|
||||
table
|
||||
border-collapse: collapse
|
||||
border-spacing: 0
|
||||
@ -0,0 +1,27 @@
|
||||
::-webkit-scrollbar
|
||||
width: 6px
|
||||
background-color: rgba(0, 0, 0, 0)
|
||||
border-radius: 100px
|
||||
|
||||
opacity: 0.2
|
||||
|
||||
transition: background-color 300ms ease, opacity 500ms ease
|
||||
|
||||
&:hover
|
||||
background-color: rgba(0, 0, 0, 0.09)
|
||||
opacity: 1
|
||||
|
||||
::-webkit-scrollbar-thumb:vertical
|
||||
/* This is the EXACT color of Mac OS scrollbars. */
|
||||
background-color: rgba(0, 0, 0, 0.5)
|
||||
border-radius: 100px
|
||||
|
||||
opacity: 0.2
|
||||
|
||||
transition: background-color 300ms ease, opacity 500ms ease
|
||||
|
||||
&:active
|
||||
background-color: rgba(0, 0, 0, 0.61)
|
||||
border-radius: 100px
|
||||
|
||||
opacity: 1
|
||||
@ -0,0 +1,14 @@
|
||||
.disabled
|
||||
opacity: 0.4
|
||||
|
||||
.disabled:hover
|
||||
cursor: not-allowed !important
|
||||
|
||||
.noselect
|
||||
-webkit-user-select: none
|
||||
|
||||
.hidden
|
||||
display: none !important
|
||||
|
||||
.invisible
|
||||
visibility: hidden !important
|
||||
1280
lightdm/themes/lightdm-webkit-theme-aether/src/sass/style.sass
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.4 KiB |