Compare commits
74 Commits
Author | SHA1 | Date |
---|---|---|
Darks | fbc500e833 | |
Darks | 87d795959f | |
Darks | 1112decbd0 | |
Lephe | b9abfcfde7 | |
Lephe | 3551103c88 | |
Lephe | be644a6e18 | |
Lephe | 76f03be60f | |
Lephe | a6689312cf | |
Lephe | 306f1bab64 | |
Lephe | 9f0cbc810b | |
Lephe | b8ed0bba99 | |
Lephe | 9727c2a986 | |
Lephe | 7423e522c3 | |
Lephe | d10f9b3b48 | |
Lephe | b533f8a161 | |
Darks | 44d2a59cbe | |
Lephe | b1ca1c4bbc | |
Lephe | 2d6007ead4 | |
Lephe | 7fc4424812 | |
Lephe | 1837f8f9a6 | |
Lephe | 888006cf86 | |
Lephe | 08a8be1766 | |
Lephe | 6532dc8940 | |
Lephe | 64a6f270c5 | |
Lephe | 59236cac70 | |
Lephe | c4c9421beb | |
Lephe | b31ed9bec2 | |
Darks | f848615ff1 | |
Lephe | 05f56a1cf4 | |
Lephe | 619ea85eeb | |
Lephe | c8c030081b | |
Eragon | 78b6448167 | |
Lephe | 2aa2fd539f | |
Lephe | 7b948979aa | |
Lephe | 47a1e1fc65 | |
Lephe | 7301aa954b | |
Lephe | b3d99b93f8 | |
Lephe | 8f0e15029c | |
Lephe | 61f48259f2 | |
Lephe | 5904019048 | |
Lephe | cc5f4e481b | |
Lephe | bee912f88c | |
Lephe | 3c5599adf7 | |
Lephe | 3876e3470e | |
Lephe | fe039e4092 | |
Lephe | e29c73d09e | |
Lephe | f75f1618bc | |
Lephe | c59e844852 | |
Lephe | b00d44ddc1 | |
Lephe | daadd21877 | |
Eragon | 6cbf5a51f9 | |
Darks | 55beccf7f4 | |
Darks | 9afdc63a8e | |
Lephe | 4be0e1572c | |
Darks | d7d8244da9 | |
Lephenixnoir | 40aeb1674e | |
Darks | 8bdf3909ea | |
Darks | 87ef91b9e3 | |
Darks | d783feb7d0 | |
Lephe | 284e8986fa | |
Lephe | 07a91c4663 | |
Lephe | 1d63d05c1e | |
Lephe | f5f1a445ea | |
Lephe | 6f98cba65e | |
Lephe | d50b58cd24 | |
Lephe | 3ee3794818 | |
Darks | 0edc996287 | |
Darks | 41d1411f86 | |
Darks | f722d700c2 | |
Darks | 53afccf2a3 | |
Darks | 6136c6e9bf | |
Darks | eba1b7dd3b | |
Eldeberen | 7e7e865430 | |
Eldeberen | fecbdffee8 |
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
@ -0,0 +1,13 @@
|
|||
CSSC := lessc
|
||||
src := $(wildcard app/static/less/*.less)
|
||||
obj := $(src:app/static/less/%.less=app/static/css/%.css)
|
||||
|
||||
run: css
|
||||
@flask run
|
||||
|
||||
css: $(obj)
|
||||
|
||||
app/static/css/%.css: app/static/less/%.less
|
||||
$(CSSC) $< $@
|
||||
|
||||
.PHONY: css run
|
|
@ -9,3 +9,7 @@ Don't be an asshole.
|
|||
* On respecte la PEP8. Je sais c'est relou d'indenter avec des espaces, mais au moins le reste est consistant.
|
||||
* La seule exception concerne la longueur des lignes. Merci d'essayer de respecter les 79 colonnes, mais dans certains cas c'est plus crade de revenir à la ligne, donc blc.
|
||||
* Je conseille d'utiliser Flake8 qui permet de vérifier les erreurs de syntaxe, de style, etc. en live.
|
||||
|
||||
### License
|
||||
|
||||
Le code de Planète Casio v5 est sous license GPLv3+. Voyez [`LICENSE`](LICENSE).
|
||||
|
|
|
@ -6,6 +6,7 @@ Attention, l'environnement est sous `python3`. Vérifiez que ce soit bien le cas
|
|||
La liste de paquets fourni est pour Archlinux, les paquets peuvent avoir des noms légèrement différents dans votre distribution.
|
||||
```
|
||||
python3
|
||||
python-email-validator
|
||||
python-flask
|
||||
python-flask-login
|
||||
python-flask-mail
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
# This file is a list of forums to create when setting up Planète Casio.
|
||||
#
|
||||
# * Keys are used as URLs paths and for unique identification.
|
||||
# * Prefixes represent the privilege category for a forum. Owning privileges
|
||||
# with this prefix allows the user to post in this forum and all its
|
||||
# sub-forum regardless of their settings ("forum-root-*" are hyper powerful).
|
||||
# * For open forums, use the prefix "open".
|
||||
# * Prefixes are used to identify privileges for each forum, see groups.yaml
|
||||
# for details.
|
||||
|
||||
/:
|
||||
name: Forum de Planète Casio
|
||||
|
@ -104,8 +102,8 @@
|
|||
prefix: discussion
|
||||
descr: Sujets hors-sujet et discussion libre.
|
||||
|
||||
# Limited-access board
|
||||
# Prefixes "admin" and "assoc" are reserved for this and require special
|
||||
# Limited-access boards
|
||||
# Prefixes "admin" and "creativecalc" are reserved for this and require special
|
||||
# privileges to list, read and edit topics and messages.
|
||||
|
||||
/admin:
|
||||
|
@ -116,5 +114,5 @@
|
|||
|
||||
/creativecalc:
|
||||
name: CreativeCalc
|
||||
prefix: assoc
|
||||
prefix: creativecalc
|
||||
descr: Forum privé de l'association CreativeCalc, réservé aux membres.
|
||||
|
|
|
@ -1,63 +1,109 @@
|
|||
# LIST OF PRIVILEGES:
|
||||
#
|
||||
# Access to specific forums (see forums.yaml for prefix values):
|
||||
# forum.access.<prefix>
|
||||
# forum.post.<prefix>
|
||||
# forum.post-news
|
||||
# forum.post-anywhere
|
||||
# -> All forums are readable by default except <admin> and <creativecalc>
|
||||
# -> All forums are writable by default except <admin>, <creativecalc>,
|
||||
# children of <news>, and forums with children ("categories")
|
||||
# -> Use member.can_access_forum(forum) and member.can_post_in_forum(forum)
|
||||
#
|
||||
# Access to extended publication methods:
|
||||
# publish.schedule-posts
|
||||
# publish.pin-posts
|
||||
# publish.shared-files
|
||||
#
|
||||
# Moderation:
|
||||
# edit.posts (includes top comment selection)
|
||||
# edit.tests
|
||||
# edit.accounts
|
||||
# edit.trophies
|
||||
# delete.posts (includes triple XP removal)
|
||||
# delete.tests
|
||||
# delete.accounts
|
||||
# delete.shared-files
|
||||
# move.posts
|
||||
#
|
||||
# Shoutbox:
|
||||
# shoutbox.kick
|
||||
# shoutbox.ban
|
||||
#
|
||||
# Miscellaneous:
|
||||
# misc.unlimited-pms
|
||||
# misc.dev-infos
|
||||
# misc.arbitrary-login
|
||||
# misc.community-login
|
||||
# misc.admin-panel
|
||||
# misc.no-upload-limits
|
||||
#
|
||||
# TODO: PRIVILEGES NOT YET IMPLEMENTED:
|
||||
# The features that these privileges control are not implemented yet, or the
|
||||
# privilege checks are missing.
|
||||
#
|
||||
# publish.*
|
||||
# edit.tests
|
||||
# delete.tests delete.shared-files
|
||||
# move.posts
|
||||
# shoutbox.*
|
||||
# misc.unlimited-pms
|
||||
|
||||
-
|
||||
name: Administrateur
|
||||
css: "color: #ee0000;"
|
||||
descr: "Vous voyez Chuck Norris ? Pareil."
|
||||
privs: access-admin-board access-assoc-board write-news write-anywhere
|
||||
upload-shared-files delete-shared-files
|
||||
edit-posts delete-posts scheduled-posting
|
||||
delete-content move-public-content move-private-content showcase-content
|
||||
edit-static-content extract-posts
|
||||
delete-notes delete-tests
|
||||
shoutbox-kick shoutbox-ban
|
||||
unlimited-pms footer-statistics community-login
|
||||
access-admin-panel edit-account delete-account edit-trophies
|
||||
delete_notification no-upload-limits
|
||||
privs: forum.access.admin forum.access.creativecalc forum.post-news
|
||||
forum.post-anywhere
|
||||
publish.schedule-posts publish.pin-posts publish.shared-files
|
||||
edit.posts edit.tests edit.accounts edit.trophies
|
||||
delete.posts delete.tests delete.accounts delete.shared-files
|
||||
move.posts
|
||||
shoutbox.kick shoutbox.ban
|
||||
misc.unlimited-pms misc.dev-infos misc.admin-panel
|
||||
misc.no-upload-limits misc.arbitrary-login
|
||||
-
|
||||
name: Modérateur
|
||||
css: "color: green;"
|
||||
descr: "Maîtres du kick, ils sont là pour faire respecter un semblant d'ordre."
|
||||
privs: access-admin-board
|
||||
edit-posts delete-posts
|
||||
move-public-content extract-posts
|
||||
delete-notes delete-tests
|
||||
shoutbox-kick shoutbox-ban
|
||||
unlimited-pms no-upload-limits
|
||||
privs: forum.access.admin
|
||||
edit.posts edit.tests
|
||||
delete.posts delete.tests
|
||||
move.posts
|
||||
shoutbox.kick shoutbox.ban
|
||||
misc.unlimited-pms misc.no-upload-limits
|
||||
-
|
||||
name: Développeur
|
||||
css: "color: #4169e1;"
|
||||
descr: "Les développeurs maintiennent et améliorent le code du site."
|
||||
privs: access-admin-board
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
edit-static-content
|
||||
unlimited-pms footer-statistics community-login
|
||||
access-admin-panel no-upload-limits
|
||||
privs: forum.access.admin forum.post-anywhere
|
||||
publish.schedule-posts publish.shared-files
|
||||
delete.shared-files
|
||||
misc.unlimited-pms misc.dev-infos misc.community-login misc.admin-panel
|
||||
-
|
||||
name: Rédacteur
|
||||
css: "color: blue;"
|
||||
descr: "Rédigent les meilleurs articles de la page d'accueil, rien que pour
|
||||
vous <3"
|
||||
privs: access-admin-board write-news
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
showcase-content edit-static-content
|
||||
no-upload-limits
|
||||
privs: forum.access.admin forum.post-news
|
||||
publish.schedule-posts publish.pin-posts publish.shared-files
|
||||
delete.shared-files
|
||||
misc.no-upload-limits misc.community-login
|
||||
-
|
||||
name: Responsable communauté
|
||||
css: "color: DarkOrange;"
|
||||
descr: "Anime les pages Twitter et Facebook de Planète Casio et surveille
|
||||
l'évolution du monde autour de nous !"
|
||||
privs: access-admin-board write-news
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
showcase-content
|
||||
privs: forum.access.admin forum.post-news
|
||||
publish.schedule-posts publish.pin-posts publish.shared-files
|
||||
delete.shared-files misc.community-login
|
||||
-
|
||||
name: Partenaire
|
||||
css: "color: purple;"
|
||||
descr: "Membres de l'équipe d'administration des sites partenaires."
|
||||
privs: write-news
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
privs: forum.post-news
|
||||
publish.schedule-posts publish.shared-files
|
||||
delete.shared-files
|
||||
-
|
||||
name: Compte communautaire
|
||||
css: "background:#d8d8d8; border-radius:4px; color:#303030; padding:1px 2px;"
|
||||
|
@ -66,13 +112,13 @@
|
|||
name: Robot
|
||||
css: "color: #cf25d0;"
|
||||
descr: "♫ Je suis Nono, le petit robot, l'ami d'Ulysse ♫"
|
||||
privs: shoutbox-post shoutbox-kick shoutbox-ban
|
||||
privs: shoutbox.kick shoutbox.ban
|
||||
-
|
||||
name: Membre de CreativeCalc
|
||||
css: "color: #222222;"
|
||||
descr: "CreativeCalc est l'association qui gère Planète Casio."
|
||||
privs: access-assoc-board
|
||||
privs: forum.access.creativecalc
|
||||
-
|
||||
name: No login
|
||||
css: "color: #888888;"
|
||||
descr: "Compte dont l'accès au site est désactivé."
|
||||
name: No login
|
||||
css: "color: #888888;"
|
||||
descr: "Compte dont l'accès au site est désactivé."
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
-
|
||||
name: Programmeur du dimanche
|
||||
is_title: False
|
||||
description: Publier 5 prorammes.
|
||||
description: Publier 5 programmes.
|
||||
hidden: False
|
||||
-
|
||||
name: Codeur invétéré
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField, DecimalField, SelectField
|
||||
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField, DecimalField, SelectField, RadioField
|
||||
from wtforms.fields.html5 import DateField, EmailField
|
||||
from wtforms.validators import InputRequired, Optional, Email, EqualTo
|
||||
from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty
|
||||
|
@ -10,271 +10,177 @@ class RegistrationForm(FlaskForm):
|
|||
username = StringField(
|
||||
'Pseudonyme',
|
||||
description='Ce nom est définitif !',
|
||||
validators=[
|
||||
InputRequired(),
|
||||
vd.name.valid,
|
||||
vd.name.available,
|
||||
],
|
||||
)
|
||||
validators=[InputRequired(), vd.name.valid, vd.name.available])
|
||||
|
||||
email = EmailField(
|
||||
'Adresse Email',
|
||||
validators=[
|
||||
InputRequired(),
|
||||
Email(message="Adresse email invalide."),
|
||||
vd.email,
|
||||
],
|
||||
)
|
||||
vd.email
|
||||
])
|
||||
|
||||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
InputRequired(),
|
||||
vd.password.is_strong,
|
||||
],
|
||||
)
|
||||
validators=[InputRequired(), vd.password.is_strong])
|
||||
|
||||
password2 = PasswordField(
|
||||
'Répéter le mot de passe',
|
||||
validators=[
|
||||
InputRequired(),
|
||||
EqualTo('password', message="Les mots de passe doivent être identiques."),
|
||||
],
|
||||
)
|
||||
EqualTo('password', message="Les mots de passe doivent être identiques.")
|
||||
])
|
||||
|
||||
guidelines = BooleanField(
|
||||
"""J'accepte les <a href="#">CGU</a>""",
|
||||
validators=[
|
||||
InputRequired(),
|
||||
],
|
||||
)
|
||||
validators=[InputRequired()])
|
||||
|
||||
newsletter = BooleanField(
|
||||
'Inscription à la newsletter',
|
||||
description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.',
|
||||
)
|
||||
submit = SubmitField(
|
||||
"S'inscrire",
|
||||
)
|
||||
description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.')
|
||||
|
||||
submit = SubmitField("S'inscrire")
|
||||
|
||||
|
||||
class UpdateAccountForm(FlaskForm):
|
||||
class UpdateAccountBaseForm(FlaskForm):
|
||||
avatar = FileField(
|
||||
'Avatar',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.file.is_image,
|
||||
vd.file.avatar_size,
|
||||
],
|
||||
)
|
||||
validators=[Optional(), vd.file.is_image, vd.file.avatar_size])
|
||||
|
||||
email = EmailField(
|
||||
'Adresse email',
|
||||
validators=[
|
||||
Optional(),
|
||||
Email(message="Addresse email invalide."),
|
||||
vd.email,
|
||||
vd.password.old_password,
|
||||
],
|
||||
)
|
||||
# For users: vd.password.old_password is added dynamically
|
||||
])
|
||||
|
||||
password = PasswordField(
|
||||
'Nouveau mot de passe',
|
||||
description="L'ancien mot de passe ne pourra pas être récupéré !",
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.password.is_strong,
|
||||
vd.password.old_password,
|
||||
],
|
||||
)
|
||||
password2 = PasswordField(
|
||||
'Répéter le mot de passe',
|
||||
validators=[
|
||||
Optional(),
|
||||
EqualTo('password', message="Les mots de passe doivent être identiques."),
|
||||
],
|
||||
)
|
||||
old_password = PasswordField(
|
||||
'Mot de passe actuel',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
# For users: vd.password.old_password is added dynamically
|
||||
])
|
||||
|
||||
birthday = DateField(
|
||||
'Anniversaire',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
validators=[Optional()])
|
||||
|
||||
signature = TextAreaField(
|
||||
'Signature',
|
||||
validators=[
|
||||
Optional(),
|
||||
]
|
||||
)
|
||||
validators=[Optional()])
|
||||
|
||||
biography = TextAreaField(
|
||||
'Présentation',
|
||||
validators=[
|
||||
Optional(),
|
||||
]
|
||||
)
|
||||
validators=[Optional()])
|
||||
|
||||
title = SelectField(
|
||||
'Titre',
|
||||
coerce=int,
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.own_title,
|
||||
]
|
||||
)
|
||||
# For users: vd.own_title (admins can assign any title!)
|
||||
])
|
||||
|
||||
newsletter = BooleanField(
|
||||
'Inscription à la newsletter',
|
||||
description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.',
|
||||
)
|
||||
description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.')
|
||||
|
||||
submit = SubmitField('Mettre à jour')
|
||||
|
||||
|
||||
class DeleteAccountForm(FlaskForm):
|
||||
class UpdateAccountForm(UpdateAccountBaseForm):
|
||||
password2 = PasswordField(
|
||||
'Répéter le mot de passe',
|
||||
validators=[
|
||||
Optional(),
|
||||
EqualTo('password', message="Les mots de passe doivent être identiques.")
|
||||
])
|
||||
|
||||
old_password = PasswordField(
|
||||
'Mot de passe actuel',
|
||||
validators=[Optional()])
|
||||
|
||||
theme = RadioField(
|
||||
'Thème du site',
|
||||
choices=[
|
||||
('default_theme', 'Planète Casio v5'),
|
||||
('FK_dark_theme', 'Thème sombre (FlamingKite)'),
|
||||
('Tituya_v43_theme', 'Thème Planète Casio v4 (Tituya)'),
|
||||
])
|
||||
|
||||
|
||||
class AdminUpdateAccountForm(UpdateAccountBaseForm):
|
||||
username = StringField(
|
||||
'Pseudonyme',
|
||||
validators=[Optional(), vd.name.valid, vd.name.available])
|
||||
|
||||
email_confirmed = BooleanField(
|
||||
"Confirmer l'email",
|
||||
description="Si décoché, l'utilisateur devra demander explicitement un"
|
||||
" email de validation, ou faire valider son adresse email par un"
|
||||
" administrateur.")
|
||||
|
||||
xp = DecimalField(
|
||||
'XP',
|
||||
validators=[Optional()])
|
||||
|
||||
|
||||
class DeleteAccountBaseForm(FlaskForm):
|
||||
transfer = BooleanField(
|
||||
'Conserver les posts sous forme anonyme',
|
||||
description="Aucune information personnelle n'est conservée ; seul le texte de posts reste. Cela permet de garder un historique fidèle des échanges.",
|
||||
default=True)
|
||||
|
||||
delete = BooleanField(
|
||||
'Confirmer la suppression',
|
||||
validators=[
|
||||
InputRequired(),
|
||||
],
|
||||
description='Attention, cette opération est irréversible !'
|
||||
)
|
||||
validators=[InputRequired()],
|
||||
description='Attention, cette opération est irréversible !')
|
||||
|
||||
submit = SubmitField('Supprimer le compte')
|
||||
|
||||
|
||||
class DeleteAccountForm(DeleteAccountBaseForm):
|
||||
old_password = PasswordField(
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
InputRequired(),
|
||||
vd.password.old_password,
|
||||
],
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Supprimer le compte',
|
||||
)
|
||||
validators=[InputRequired(), vd.password.old_password])
|
||||
|
||||
|
||||
class AdminDeleteAccountForm(DeleteAccountBaseForm):
|
||||
pass
|
||||
|
||||
|
||||
class AskResetPasswordForm(FlaskForm):
|
||||
email = EmailField(
|
||||
'Adresse email',
|
||||
validators=[
|
||||
Optional(),
|
||||
Email(message="Addresse email invalide."),
|
||||
],
|
||||
)
|
||||
validators=[Optional(), Email(message="Addresse email invalide.")])
|
||||
|
||||
submit = SubmitField('Valider')
|
||||
|
||||
|
||||
class ResetPasswordForm(FlaskForm):
|
||||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.password.is_strong,
|
||||
],
|
||||
)
|
||||
validators=[Optional(), vd.password.is_strong])
|
||||
|
||||
password2 = PasswordField(
|
||||
'Répéter le mot de passe',
|
||||
validators=[
|
||||
Optional(),
|
||||
EqualTo('password', message="Les mots de passe doivent être identiques."),
|
||||
],
|
||||
)
|
||||
EqualTo('password', message="Les mots de passe doivent être identiques.")
|
||||
])
|
||||
|
||||
submit = SubmitField('Valider')
|
||||
|
||||
|
||||
class AdminUpdateAccountForm(FlaskForm):
|
||||
username = StringField(
|
||||
'Pseudonyme',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.name.valid,
|
||||
vd.name.available,
|
||||
],
|
||||
)
|
||||
avatar = FileField(
|
||||
'Avatar',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.file.is_image,
|
||||
vd.file.avatar_size,
|
||||
],
|
||||
)
|
||||
email = EmailField(
|
||||
'Adresse email',
|
||||
validators=[
|
||||
Optional(),
|
||||
Email(message="Addresse email invalide."),
|
||||
vd.email,
|
||||
],
|
||||
)
|
||||
email_confirmed = BooleanField(
|
||||
"Confirmer l'email",
|
||||
description="Si décoché, l'utilisateur devra demander explicitement un email "
|
||||
"de validation, ou faire valider son adresse email par un administrateur.",
|
||||
)
|
||||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
description="L'ancien mot de passe ne pourra pas être récupéré !",
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.password.is_strong,
|
||||
],
|
||||
)
|
||||
xp = DecimalField(
|
||||
'XP',
|
||||
validators=[
|
||||
Optional(),
|
||||
]
|
||||
)
|
||||
birthday = DateField(
|
||||
'Anniversaire',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
signature = TextAreaField(
|
||||
'Signature',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
biography = TextAreaField(
|
||||
'Présentation',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
title = SelectField(
|
||||
'Titre',
|
||||
coerce=int,
|
||||
validators=[
|
||||
Optional(),
|
||||
# Admin can set any title to any member!
|
||||
]
|
||||
)
|
||||
newsletter = BooleanField(
|
||||
'Inscription à la newsletter',
|
||||
description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.',
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Mettre à jour',
|
||||
)
|
||||
|
||||
|
||||
class AdminAccountEditTrophyForm(FlaskForm):
|
||||
# Boolean inputs are generated on-the-fly from trophy list
|
||||
submit = SubmitField(
|
||||
'Modifier',
|
||||
)
|
||||
submit = SubmitField('Modifier')
|
||||
|
||||
|
||||
class AdminAccountEditGroupForm(FlaskForm):
|
||||
# Boolean inputs are generated on-the-fly from group list
|
||||
submit = SubmitField(
|
||||
'Modifier',
|
||||
)
|
||||
|
||||
|
||||
class AdminDeleteAccountForm(FlaskForm):
|
||||
delete = BooleanField(
|
||||
'Confirmer la suppression',
|
||||
validators=[
|
||||
InputRequired(),
|
||||
],
|
||||
description='Attention, cette opération est irréversible !',
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Supprimer le compte',
|
||||
)
|
||||
submit = SubmitField('Modifier')
|
||||
|
|
|
@ -1,22 +1,47 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField
|
||||
from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField, SelectField
|
||||
from wtforms.validators import InputRequired, Length
|
||||
import app.utils.validators as vd
|
||||
from app.utils.antibot_field import AntibotField
|
||||
|
||||
class CommentForm(FlaskForm):
|
||||
message = TextAreaField('Message', validators=[InputRequired()])
|
||||
attachments = MultipleFileField('Pièces-jointes',
|
||||
validators=[vd.file.optional, vd.file.count, vd.file.extension,
|
||||
vd.file.size, vd.file.namelength])
|
||||
message = TextAreaField(
|
||||
'Message',
|
||||
validators=[InputRequired()])
|
||||
|
||||
attachments = MultipleFileField(
|
||||
'Pièces-jointes',
|
||||
validators=[
|
||||
vd.file.optional,
|
||||
vd.file.count,
|
||||
vd.file.extension,
|
||||
vd.file.size,
|
||||
vd.file.namelength
|
||||
])
|
||||
|
||||
submit = SubmitField('Commenter')
|
||||
|
||||
|
||||
class AnonymousCommentForm(CommentForm):
|
||||
pseudo = StringField('Pseudo',
|
||||
pseudo = StringField(
|
||||
'Pseudo',
|
||||
validators=[InputRequired(), vd.name.valid, vd.name.available])
|
||||
|
||||
ab = AntibotField()
|
||||
|
||||
|
||||
class CommentEditForm(CommentForm):
|
||||
# Boolean fields to remove files are added dynamically
|
||||
attachments = MultipleFileField(
|
||||
'Ajouter des pièces jointes',
|
||||
validators=[
|
||||
vd.file.optional,
|
||||
vd.file.count,
|
||||
vd.file.extension,
|
||||
vd.file.size,
|
||||
vd.file.namelength
|
||||
])
|
||||
|
||||
submit = SubmitField('Modifier')
|
||||
|
||||
|
||||
|
@ -25,10 +50,25 @@ class AnonymousCommentEditForm(CommentEditForm, AnonymousCommentForm):
|
|||
|
||||
|
||||
class TopicCreationForm(CommentForm):
|
||||
title = StringField('Nom du sujet',
|
||||
title = StringField(
|
||||
'Nom du sujet',
|
||||
validators=[InputRequired(), Length(min=3, max=128)])
|
||||
|
||||
submit = SubmitField('Créer le sujet')
|
||||
|
||||
|
||||
class AnonymousTopicCreationForm(TopicCreationForm, AnonymousCommentForm):
|
||||
pass
|
||||
ab = AntibotField()
|
||||
|
||||
|
||||
class TopicEditForm(CommentEditForm):
|
||||
title = StringField(
|
||||
'Nom du sujet',
|
||||
validators=[InputRequired(), Length(min=3, max=128)])
|
||||
|
||||
# List of forums is generated at runtime
|
||||
forum = SelectField(
|
||||
'Forum',
|
||||
validators=[InputRequired()])
|
||||
|
||||
submit = SubmitField('Modifier le sujet')
|
||||
|
|
|
@ -5,13 +5,13 @@ from wtforms.validators import InputRequired
|
|||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField(
|
||||
'Identifiant',
|
||||
'Identifiant',
|
||||
validators=[
|
||||
InputRequired(),
|
||||
],
|
||||
)
|
||||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
InputRequired(),
|
||||
],
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField
|
||||
from wtforms.validators import InputRequired
|
||||
|
||||
|
||||
class LoginAsForm(FlaskForm):
|
||||
username = StringField(
|
||||
'Identifiant',
|
||||
validators=[
|
||||
InputRequired(),
|
||||
],
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Vandaliser',
|
||||
)
|
|
@ -2,6 +2,7 @@ from flask_wtf import FlaskForm
|
|||
from wtforms import StringField, SubmitField, BooleanField
|
||||
from wtforms.validators import InputRequired, Optional
|
||||
from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty
|
||||
import app.utils.validators
|
||||
|
||||
|
||||
class TrophyForm(FlaskForm):
|
||||
|
@ -34,6 +35,9 @@ class TrophyForm(FlaskForm):
|
|||
css = StringField(
|
||||
'CSS',
|
||||
description='CSS appliqué au titre, le cas échéant.',
|
||||
validators=[
|
||||
app.utils.validators.css,
|
||||
],
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Envoyer',
|
||||
|
|
|
@ -45,6 +45,10 @@ class Attachment(db.Model):
|
|||
|
||||
def delete_file(self):
|
||||
try:
|
||||
os.delete(self.path)
|
||||
os.remove(self.path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def delete(self):
|
||||
self.delete_file()
|
||||
db.session.delete(self)
|
||||
|
|
|
@ -15,11 +15,13 @@ class Comment(Post):
|
|||
|
||||
# Parent thread
|
||||
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'),
|
||||
nullable=False)
|
||||
nullable=False, index=True)
|
||||
thread = db.relationship('Thread',
|
||||
backref=backref('comments', lazy='dynamic'),
|
||||
foreign_keys=thread_id)
|
||||
|
||||
# attachments (relation from Attachment)
|
||||
|
||||
|
||||
def __init__(self, author, text, thread):
|
||||
"""
|
||||
|
@ -43,7 +45,9 @@ class Comment(Post):
|
|||
|
||||
def delete(self):
|
||||
"""Recursively delete post and all associated contents."""
|
||||
# FIXME: Attached files?
|
||||
for a in self.attachments:
|
||||
a.delete()
|
||||
db.session.commit()
|
||||
db.session.delete(self)
|
||||
|
||||
def __repr__(self):
|
||||
|
|
|
@ -20,7 +20,8 @@ class Forum(db.Model):
|
|||
lazy=True, foreign_keys=parent_id)
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
# <topics> List of topics in this exact forum (of type Topic)
|
||||
# <sub_forums> Children forums
|
||||
# <topics> List of topics in this exact forum (of type Topic)
|
||||
|
||||
# Some configuration
|
||||
TOPICS_PER_PAGE = 30
|
||||
|
@ -36,6 +37,19 @@ class Forum(db.Model):
|
|||
else:
|
||||
self.parent = parent
|
||||
|
||||
def is_news(self):
|
||||
"""Whether this forum is a news board."""
|
||||
return (self.parent is not None) and (self.parent.prefix == "news")
|
||||
|
||||
def is_default_accessible(self):
|
||||
"""Whether this forum can be read without privileges."""
|
||||
return (self.prefix != "admin") and (self.prefix != "creativecalc")
|
||||
|
||||
def is_default_postable(self):
|
||||
"""Whether this forum can be posted to without privileges."""
|
||||
return self.is_default_accessible() and (not self.is_news()) and \
|
||||
(self.sub_forums == [])
|
||||
|
||||
def post_count(self):
|
||||
"""Number of posts in every topic of the forum, without subforums."""
|
||||
# TODO: optimize this with real ORM
|
||||
|
|
|
@ -22,3 +22,6 @@ class Notification(db.Model):
|
|||
|
||||
def __repr__(self):
|
||||
return f'<Notification to {self.owner.name}: {self.text} ({self.href})>'
|
||||
|
||||
def delete(self):
|
||||
db.session.delete(self)
|
||||
|
|
|
@ -57,13 +57,10 @@ class Poll(db.Model):
|
|||
|
||||
def delete(self):
|
||||
"""Deletes a poll and its answers"""
|
||||
# TODO: move this out of class definition?
|
||||
for answer in SpecialPrivilege.query.filter_by(poll_id=self.id).all():
|
||||
db.session.delete(answer)
|
||||
for a in self.answers:
|
||||
db.session.delete(a)
|
||||
db.session.commit()
|
||||
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
# Common properties and methods
|
||||
@property
|
||||
|
|
|
@ -20,8 +20,6 @@ class Post(db.Model):
|
|||
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
author = db.relationship('User', backref="posts", foreign_keys=author_id)
|
||||
|
||||
# TODO: Post attachments?
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': __tablename__,
|
||||
'polymorphic_on': type
|
||||
|
|
|
@ -40,5 +40,8 @@ class Program(Post):
|
|||
p = Program(topic.author, topic.title, topic.thread)
|
||||
topic.promotion = p
|
||||
|
||||
def delete(self):
|
||||
db.session.delete(self)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Program: #{self.id} "{self.title}">'
|
||||
|
|
|
@ -9,7 +9,8 @@ class Thread(db.Model):
|
|||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Top comment
|
||||
top_comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'))
|
||||
top_comment_id = db.Column(db.Integer,
|
||||
db.ForeignKey('comment.id', use_alter=True))
|
||||
top_comment = db.relationship('Comment', foreign_keys=top_comment_id)
|
||||
|
||||
# Post owning the thread, set only by Topic, Program, etc. In general, you
|
||||
|
|
|
@ -9,6 +9,8 @@ class Topic(Post):
|
|||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': __tablename__,
|
||||
# Because there is an extra relation to Post (promotion), SQLAlchemy
|
||||
# cannot guess which Post we inherit from; specify here.
|
||||
'inherit_condition': id == Post.id
|
||||
}
|
||||
|
||||
|
@ -52,7 +54,8 @@ class Topic(Post):
|
|||
|
||||
def delete(self):
|
||||
"""Recursively delete topic and all associated contents."""
|
||||
self.thread.delete()
|
||||
if self.promotion is None:
|
||||
self.thread.delete()
|
||||
db.session.delete(self)
|
||||
|
||||
def __repr__(self):
|
||||
|
|
|
@ -28,6 +28,13 @@ class Trophy(db.Model):
|
|||
self.description = description
|
||||
self.hidden = hidden
|
||||
|
||||
def delete(self):
|
||||
for owner in self.owners:
|
||||
owner.del_trophy(self)
|
||||
db.session.add(owner)
|
||||
db.session.commit()
|
||||
db.session.delete(self)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Trophy: {self.name}>'
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@ import os
|
|||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
""" Website user that performs actions on the post """
|
||||
""" Any website user, logged in (Member) or not (Guest) """
|
||||
|
||||
__tablename__ = 'user'
|
||||
|
||||
# User ID, should be used to refer to any user. Thea actual user can either
|
||||
# User ID, should be used to refer to any user. The actual user can either
|
||||
# be a guest (with IP as key) or a member (with this ID as key).
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# User type (polymorphic discriminator)
|
||||
|
@ -55,18 +55,18 @@ class Guest(User):
|
|||
# ID of the [User] entry
|
||||
id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||
# Reusable username, cannot be chosen as the name of a member
|
||||
# but will be distinguished at rendering time if a member take it later
|
||||
# but will be distinguished at rendering time if a member takes it later
|
||||
name = db.Column(db.Unicode(User.NAME_MAXLEN))
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Guest: {self.username}>'
|
||||
return f'<Guest: {self.name}>'
|
||||
|
||||
|
||||
class Member(User):
|
||||
""" Registered user with full access to the website's services """
|
||||
""" Registered user with full access to the site's features """
|
||||
|
||||
__tablename__ = 'member'
|
||||
__mapper_args__ = {'polymorphic_identity': __tablename__}
|
||||
|
@ -108,6 +108,7 @@ class Member(User):
|
|||
|
||||
# Settings
|
||||
newsletter = db.Column(db.Boolean, default=False)
|
||||
theme = db.Column(db.Unicode(32))
|
||||
|
||||
# Relations
|
||||
trophies = db.relationship('Trophy', secondary=TrophyMember,
|
||||
|
@ -116,10 +117,6 @@ class Member(User):
|
|||
programs = db.relationship('Program')
|
||||
comments = db.relationship('Comment')
|
||||
|
||||
# Displayed title
|
||||
# title_id = db.Column(db.Integer, db.ForeignKey('title.id'))
|
||||
# title = db.relationship('Title', foreign_keys=title_id)
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
# <notifications> List of unseen notifications (of type Notification)
|
||||
# <polls> Polls created by the member (of class Poll)
|
||||
|
@ -135,22 +132,57 @@ class Member(User):
|
|||
# Workflow with LDAP enabled is User → Postgresql → LDAP → set password
|
||||
self.xp = 0
|
||||
|
||||
self.theme = 'default_theme'
|
||||
self.bio = ""
|
||||
self.signature = ""
|
||||
self.birthday = None
|
||||
|
||||
def generate_guest_name(self):
|
||||
"""Generates a unique guest name to transfer contents to."""
|
||||
count = 0
|
||||
while Guest.query.filter_by(name=f"{self.name}_{count}").first():
|
||||
count += 1
|
||||
return f"{self.name}_{count}"
|
||||
|
||||
def transfer_posts(self, other):
|
||||
"""
|
||||
Transfers all the posts to another user. This is generally used to
|
||||
transfer ownership to a newly-created Guest before deleting an account.
|
||||
"""
|
||||
for t in self.topics:
|
||||
t.author = other
|
||||
db.session.add(t)
|
||||
for p in self.programs:
|
||||
p.author = other
|
||||
db.session.add(p)
|
||||
for c in self.comments:
|
||||
c.author = other
|
||||
db.session.add(c)
|
||||
|
||||
def delete_posts(self):
|
||||
"""Deletes the user's posts."""
|
||||
for t in self.topics:
|
||||
t.delete()
|
||||
for p in self.programs:
|
||||
p.delete()
|
||||
for c in self.comments:
|
||||
c.delete()
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes the user and the associated information:
|
||||
* Special privileges
|
||||
Deletes the user, but not the posts; use either transfer_posts() or
|
||||
delete_posts() before calling this.
|
||||
"""
|
||||
|
||||
for sp in SpecialPrivilege.query.filter_by(mid=self.id).all():
|
||||
db.session.delete(sp)
|
||||
|
||||
self.trophies = []
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
# Privilege checks
|
||||
|
||||
def priv(self, priv):
|
||||
"""Check whether the member has the specified privilege."""
|
||||
|
@ -166,6 +198,46 @@ class Member(User):
|
|||
sp = SpecialPrivilege.query.filter_by(mid=self.id).all()
|
||||
return sorted(row.priv for row in sp)
|
||||
|
||||
def can_access_forum(self, forum):
|
||||
"""Whether this member can read the forum's contents."""
|
||||
return forum.is_default_accessible() or \
|
||||
self.priv(f"forum.access.{forum.prefix}")
|
||||
|
||||
def can_post_in_forum(self, forum):
|
||||
"""Whether this member can post in the forum."""
|
||||
return forum.is_default_postable() or \
|
||||
(forum.is_news() and self.priv("forum.post-news")) or \
|
||||
self.priv("forum.post.{forum.prefix}") or \
|
||||
self.priv("forum.post-anywhere")
|
||||
|
||||
def can_access_post(self, post):
|
||||
"""Whether this member can access the post's forum (if any)."""
|
||||
if post.type == "comment" and post.thread.owner_topic:
|
||||
return self.can_access_forum(post.thread.owner_post.forum)
|
||||
# Posts from other types of content are all public
|
||||
return True
|
||||
|
||||
def can_edit_post(self, post):
|
||||
"""Whether this member can edit the post."""
|
||||
return self.can_access_post(post) and \
|
||||
((post.author == self) or self.priv("edit.posts"))
|
||||
|
||||
def can_delete_post(self, post):
|
||||
"""Whether this member can delete the post."""
|
||||
return self.can_access_post(post) and \
|
||||
((post.author == self) or self.priv("delete.posts"))
|
||||
|
||||
def can_punish_post(self, post):
|
||||
"""Whether this member can delete the post with penalty."""
|
||||
return self.can_access_post(post) and self.priv("delete.posts")
|
||||
|
||||
def can_set_topcomment(self, comment):
|
||||
"""Whether this member can designate the comment as top comment."""
|
||||
if comment.type != "comment":
|
||||
return False
|
||||
post = comment.thread.owner_post
|
||||
return self.can_edit_post(post) and (comment.author == post.author)
|
||||
|
||||
def update(self, **data):
|
||||
"""
|
||||
Update all or part of the user's metadata. The [data] dictionary
|
||||
|
@ -179,6 +251,7 @@ class Member(User):
|
|||
"newsletter" bool Newsletter setting
|
||||
"xp" int Experience points
|
||||
"avatar" File Avatar image
|
||||
"theme" str Name of theme file
|
||||
For future compatibility, other attributes are silently ignored. None
|
||||
values can be specified and are ignored.
|
||||
|
||||
|
@ -212,6 +285,8 @@ class Member(User):
|
|||
self.set_avatar(data["avatar"])
|
||||
if "title" in data:
|
||||
self.title = Title.query.get(data["title"])
|
||||
if "theme" in data:
|
||||
self.theme = data["theme"]
|
||||
|
||||
# For admins only
|
||||
if "email_confirmed" in data:
|
||||
|
@ -363,12 +438,8 @@ class Member(User):
|
|||
else:
|
||||
self.del_trophy(trophies[level])
|
||||
|
||||
if context in ["new-post", "new-program", "new-tutorial", "new-test",
|
||||
None]:
|
||||
# FIXME: Use ORM tools with careful, non-circular imports
|
||||
post_count = db.session.execute(f"""SELECT COUNT(*) FROM post
|
||||
INNER JOIN member ON member.id = post.author_id
|
||||
WHERE member.id = {self.id}""").first()[0]
|
||||
if context in ["new-post","new-program","new-tutorial","new-test",None]:
|
||||
post_count = len(self.posts)
|
||||
|
||||
levels = {
|
||||
20: "Premiers mots",
|
||||
|
@ -379,7 +450,7 @@ class Member(User):
|
|||
progress(levels, post_count)
|
||||
|
||||
if context in ["new-program", None]:
|
||||
program_count = self.programs.count()
|
||||
program_count = len(self.programs)
|
||||
|
||||
levels = {
|
||||
5: "Programmeur du dimanche",
|
||||
|
|
|
@ -5,4 +5,4 @@ from app import app
|
|||
@app.before_request
|
||||
def request_time():
|
||||
g.request_start_time = time()
|
||||
g.request_time = lambda: "%.5fs" % (time() - g.request_start_time)
|
||||
g.request_time = lambda: time() - g.request_start_time
|
||||
|
|
|
@ -2,6 +2,7 @@ from app import app
|
|||
from flask import url_for
|
||||
from config import V5Config
|
||||
from slugify import slugify
|
||||
from app.utils.login_as import is_vandal
|
||||
|
||||
@app.context_processor
|
||||
def utilities_processor():
|
||||
|
@ -12,4 +13,5 @@ def utilities_processor():
|
|||
_url_for=lambda route, args, **other: url_for(route, **args, **other),
|
||||
V5Config=V5Config,
|
||||
slugify=slugify,
|
||||
is_vandal=is_vandal
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
from app.routes import index, search, users, tools, development
|
||||
from app.routes.account import login, account, notification, polls
|
||||
from app.routes.admin import index, groups, account, trophies, forums, \
|
||||
attachments, config, members, polls
|
||||
attachments, config, members, polls, login_as
|
||||
from app.routes.forum import index, topic
|
||||
from app.routes.polls import vote, delete
|
||||
from app.routes.posts import edit
|
||||
|
|
|
@ -3,12 +3,13 @@ from flask_login import login_required, current_user, logout_user
|
|||
from app import app, db
|
||||
from app.forms.account import UpdateAccountForm, RegistrationForm, \
|
||||
DeleteAccountForm, AskResetPasswordForm, ResetPasswordForm
|
||||
from app.models.user import Member
|
||||
from app.models.user import Guest, Member
|
||||
from app.models.trophy import Title
|
||||
from app.utils.render import render
|
||||
from app.utils.send_mail import send_validation_mail, send_reset_password_mail
|
||||
from app.utils.priv_required import guest_only
|
||||
import app.utils.ldap as ldap
|
||||
import app.utils.validators as vd
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from config import V5Config
|
||||
|
||||
|
@ -20,8 +21,15 @@ def edit_account():
|
|||
titles = [(t.id, t.name) for t in current_user.trophies if isinstance(t, Title)]
|
||||
titles.insert(0, (-1, "Membre"))
|
||||
form.title.choices = titles
|
||||
|
||||
extra_vd = {
|
||||
"email": [vd.password.old_password],
|
||||
"password": [vd.password.old_password],
|
||||
"title": [vd.own_title],
|
||||
}
|
||||
|
||||
if form.submit.data:
|
||||
if form.validate_on_submit():
|
||||
if form.is_submitted() and form.validate(extra_validators=extra_vd):
|
||||
current_user.update(
|
||||
avatar=form.avatar.data or None,
|
||||
email=form.email.data or None,
|
||||
|
@ -30,7 +38,8 @@ def edit_account():
|
|||
signature=form.signature.data,
|
||||
bio=form.biography.data,
|
||||
title=form.title.data,
|
||||
newsletter=form.newsletter.data
|
||||
newsletter=form.newsletter.data,
|
||||
theme=form.theme.data
|
||||
)
|
||||
db.session.merge(current_user)
|
||||
db.session.commit()
|
||||
|
@ -39,6 +48,8 @@ def edit_account():
|
|||
return redirect(request.url)
|
||||
else:
|
||||
flash('Erreur lors de la modification', 'error')
|
||||
else:
|
||||
form.theme.data = current_user.theme or 'default_theme'
|
||||
|
||||
return render('account/account.html', scripts=["+scripts/entropy.js"],
|
||||
form=form)
|
||||
|
@ -88,9 +99,20 @@ def reset_password(token):
|
|||
@login_required
|
||||
def delete_account():
|
||||
del_form = DeleteAccountForm()
|
||||
|
||||
if del_form.submit.data:
|
||||
if del_form.validate_on_submit():
|
||||
db.session.delete(current_user)
|
||||
if del_form.transfer.data:
|
||||
guest = Guest(current_user.generate_guest_name())
|
||||
db.session.add(guest)
|
||||
db.session.commit()
|
||||
current_user.transfer_posts(guest)
|
||||
db.session.commit()
|
||||
else:
|
||||
current_user.delete_posts()
|
||||
db.session.commit()
|
||||
|
||||
current_user.delete()
|
||||
logout_user()
|
||||
db.session.commit()
|
||||
flash('Compte supprimé', 'ok')
|
||||
|
@ -98,6 +120,7 @@ def delete_account():
|
|||
else:
|
||||
flash('Erreur lors de la suppression du compte', 'error')
|
||||
del_form.delete.data = False # Force to tick to delete the account
|
||||
|
||||
return render('account/delete_account.html', del_form=del_form)
|
||||
|
||||
|
||||
|
|
|
@ -27,11 +27,11 @@ def delete_notification(id=None):
|
|||
if notification:
|
||||
# Only current user or admin can delete notifications
|
||||
if notification.owner_id == current_user.id:
|
||||
db.session.delete(notification)
|
||||
notification.delete()
|
||||
db.session.commit()
|
||||
return redirect(url_for('list_notifications'))
|
||||
elif 'delete_notification' in current_user.privs:
|
||||
db.session.delete(notification)
|
||||
notification.delete()
|
||||
db.session.commit()
|
||||
if request.referrer:
|
||||
return redirect(request.referrer)
|
||||
|
@ -41,7 +41,7 @@ def delete_notification(id=None):
|
|||
abort(404)
|
||||
elif id == "all":
|
||||
for n in current_user.notifications:
|
||||
db.session.delete(n)
|
||||
n.delete()
|
||||
db.session.commit()
|
||||
return redirect(url_for('list_notifications'))
|
||||
# TODO: add something to allow an admin to delete all notifs for a user
|
||||
|
|
|
@ -2,7 +2,7 @@ from flask import flash, redirect, url_for, request
|
|||
from flask_login import current_user
|
||||
from wtforms import BooleanField
|
||||
from app.utils.priv_required import priv_required
|
||||
from app.models.user import Member
|
||||
from app.models.user import Guest, Member
|
||||
from app.models.trophy import Trophy, Title
|
||||
from app.models.priv import Group
|
||||
from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \
|
||||
|
@ -14,7 +14,7 @@ from config import V5Config
|
|||
|
||||
|
||||
@app.route('/admin/compte/<user_id>/editer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-account')
|
||||
@priv_required('misc.admin-panel', 'edit.accounts')
|
||||
def adm_edit_account(user_id):
|
||||
user = Member.query.filter_by(id=user_id).first_or_404()
|
||||
|
||||
|
@ -27,11 +27,13 @@ def adm_edit_account(user_id):
|
|||
|
||||
for t in Trophy.query.all():
|
||||
setattr(TrophyForm, f't{t.id}', BooleanField(t.name))
|
||||
setattr(TrophyForm, "trophies", {f't{t.id}': t for t in Trophy.query.all()})
|
||||
setattr(TrophyForm, "user_trophies", [f't{t.id}' for t in user.trophies])
|
||||
trophy_form = TrophyForm(prefix="trophies")
|
||||
|
||||
for g in Group.query.all():
|
||||
setattr(GroupForm, f'g{g.id}', BooleanField(g.name))
|
||||
setattr(GroupForm, "groups", {f'g{g.id}': g for g in Group.query.all()})
|
||||
setattr(GroupForm, "user_groups", [f'g{g.id}' for g in user.groups])
|
||||
group_form = GroupForm(prefix="group")
|
||||
|
||||
|
@ -119,24 +121,40 @@ def adm_edit_account(user_id):
|
|||
|
||||
|
||||
@app.route('/admin/compte/<user_id>/supprimer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'delete-account')
|
||||
@priv_required('misc.admin-panel', 'delete.accounts')
|
||||
def adm_delete_account(user_id):
|
||||
# A user deleting their own account will be disconnected
|
||||
user = Member.query.filter_by(id=user_id).first_or_404()
|
||||
|
||||
# Note: A user deleting their own account will be disconnected.
|
||||
# TODO: Number of comments by *other* members which will be deleted
|
||||
stats = {
|
||||
'comments': len(user.comments),
|
||||
'topics': len(user.topics),
|
||||
'programs': len(user.programs),
|
||||
'groups': len(user.groups),
|
||||
'privs': len(user.special_privileges()),
|
||||
}
|
||||
|
||||
# TODO: Add an overview of what will be deleted.
|
||||
# * How many posts will be turned into guest posts
|
||||
# * Option: purely delete the posts in question
|
||||
# * How many PMs will be deleted (can't unassign PMs)
|
||||
# * etc.
|
||||
del_form = AdminDeleteAccountForm()
|
||||
if del_form.submit.data:
|
||||
if del_form.validate_on_submit():
|
||||
if del_form.transfer.data:
|
||||
guest = Guest(user.generate_guest_name())
|
||||
db.session.add(guest)
|
||||
db.session.commit()
|
||||
user.transfer_posts(guest)
|
||||
db.session.commit()
|
||||
else:
|
||||
user.delete_posts()
|
||||
db.session.commit()
|
||||
|
||||
user.delete()
|
||||
db.session.commit()
|
||||
flash('Compte supprimé', 'ok')
|
||||
return redirect(url_for('adm'))
|
||||
else:
|
||||
flash('Erreur lors de la suppression du compte', 'error')
|
||||
del_form.delete.data = False # Force to tick to delete the account
|
||||
return render('admin/delete_account.html', user=user, del_form=del_form)
|
||||
|
||||
return render('admin/delete_account.html', user=user, stats=stats,
|
||||
del_form=del_form)
|
||||
|
|
|
@ -6,7 +6,7 @@ from app.utils.render import render
|
|||
# TODO: add pagination & moderation tools (deletion)
|
||||
|
||||
@app.route('/admin/fichiers', methods=['GET'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm_attachments():
|
||||
attachments = Attachment.query.all()
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from app import app
|
|||
from config import V5Config
|
||||
|
||||
@app.route('/admin/config', methods=['GET'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm_config():
|
||||
config = {k: getattr(V5Config, k) for k in [
|
||||
"DOMAIN", "DB_NAME", "USE_LDAP", "LDAP_ROOT", "LDAP_ENV",
|
||||
|
|
|
@ -4,7 +4,7 @@ from app.models.forum import Forum
|
|||
from app import app, db
|
||||
|
||||
@app.route('/admin/forums', methods=['GET'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm_forums():
|
||||
main_forum = Forum.query.filter_by(parent=None).first()
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import os
|
|||
|
||||
|
||||
@app.route('/admin/groupes', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm_groups():
|
||||
# Users with either groups or special privileges
|
||||
users_groups = Member.query.join(GroupMember)
|
||||
|
|
|
@ -4,6 +4,6 @@ from app import app
|
|||
|
||||
|
||||
@app.route('/admin', methods=['GET'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm():
|
||||
return render('admin/index.html')
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
from flask import request, flash, make_response, redirect, url_for, abort
|
||||
from flask_login import current_user, login_user, logout_user, login_required
|
||||
from itsdangerous import Serializer
|
||||
from itsdangerous.exc import BadSignature
|
||||
from app import app
|
||||
from app.utils.render import render
|
||||
from app.utils.login_as import is_vandal
|
||||
from app.utils.unicode_names import normalize
|
||||
from app.models.user import Member
|
||||
from app.models.priv import Group
|
||||
from app.forms.login_as import LoginAsForm
|
||||
|
||||
|
||||
@app.route("/admin/vandalisme", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def adm_login_as():
|
||||
""" Show a basic form and login as arbitrary user when asked """
|
||||
|
||||
# Basic permission
|
||||
if (not current_user.priv("misc.arbitrary-login") and
|
||||
not current_user.priv("misc.community-login")):
|
||||
abort(403)
|
||||
if is_vandal():
|
||||
flash("Vous êtes déjà authentifié", "error")
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Handle form
|
||||
form = LoginAsForm()
|
||||
if form.validate_on_submit():
|
||||
norm = normalize(form.username.data)
|
||||
user = Member.query.filter_by(norm=norm).one()
|
||||
if user is None:
|
||||
flash("Utilisateur invalide", "error")
|
||||
return render('admin/login_as.html', form=form)
|
||||
|
||||
# Apply for community login
|
||||
g = Group.query.filter_by(name="Compte communautaire").one()
|
||||
is_community = g in user.groups
|
||||
if not is_community and not current_user.priv("misc.arbitrary-login"):
|
||||
abort(403)
|
||||
|
||||
# Create a safe token to flee when needed
|
||||
s = Serializer(app.config["SECRET_KEY"])
|
||||
vandal_token = s.dumps(current_user.id)
|
||||
|
||||
# Login and display some messages
|
||||
login_user(user)
|
||||
if user.name == "GLaDOS":
|
||||
flash("Vous espérez quoi exactement ? Survivre ? "
|
||||
"Dans ce cas, évitez de me faire du mal.")
|
||||
else:
|
||||
flash(f"Connecté en tant que {user.name}")
|
||||
|
||||
# Return the response
|
||||
resp = make_response(redirect(url_for('index')))
|
||||
resp.set_cookie('vandale', vandal_token)
|
||||
return resp
|
||||
|
||||
# Else return form
|
||||
return render('admin/login_as.html', form=form)
|
||||
|
||||
@app.route("/admin/vandalisme/fuir")
|
||||
@login_required
|
||||
def adm_logout_as():
|
||||
""" Log out as a vandalized user, login back as admin """
|
||||
s = Serializer(app.config["SECRET_KEY"])
|
||||
|
||||
vandal_token = request.cookies.get('vandale')
|
||||
if vandal_token is None:
|
||||
abort(403)
|
||||
|
||||
try:
|
||||
id = s.loads(vandal_token)
|
||||
except BadSignature:
|
||||
flash("Vous avez vraiment agi de manière stupide.", "error")
|
||||
abort(403)
|
||||
|
||||
user = Member.query.get(id)
|
||||
logout_user()
|
||||
login_user(user)
|
||||
|
||||
if request.referrer:
|
||||
resp = make_response(redirect(request.referrer))
|
||||
else:
|
||||
resp = make_response(redirect(url_for('index')))
|
||||
|
||||
resp.set_cookie('vandale', '', expires=0)
|
||||
return resp
|
|
@ -6,7 +6,7 @@ from app import app, db
|
|||
|
||||
|
||||
@app.route('/admin/membres', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel')
|
||||
@priv_required('misc.admin-panel')
|
||||
def adm_members():
|
||||
users = Member.query.all()
|
||||
users = sorted(users, key = lambda x: x.name)
|
||||
|
|
|
@ -7,7 +7,7 @@ from app import app, db
|
|||
|
||||
|
||||
@app.route('/admin/trophees', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-trophies')
|
||||
@priv_required('misc.admin-panel', 'edit.trophies')
|
||||
def adm_trophies():
|
||||
form = TrophyForm()
|
||||
if request.method == "POST":
|
||||
|
@ -31,7 +31,7 @@ def adm_trophies():
|
|||
|
||||
|
||||
@app.route('/admin/trophees/<trophy_id>/editer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-trophies')
|
||||
@priv_required('misc.admin-panel', 'edit.trophies')
|
||||
def adm_edit_trophy(trophy_id):
|
||||
trophy = Trophy.query.filter_by(id=trophy_id).first_or_404()
|
||||
|
||||
|
@ -59,7 +59,7 @@ def adm_edit_trophy(trophy_id):
|
|||
|
||||
|
||||
@app.route('/admin/trophees/<trophy_id>/supprimer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-trophies')
|
||||
@priv_required('misc.admin-panel', 'edit.trophies')
|
||||
def adm_delete_trophy(trophy_id):
|
||||
trophy = Trophy.query.filter_by(id=trophy_id).first_or_404()
|
||||
|
||||
|
@ -67,8 +67,7 @@ def adm_delete_trophy(trophy_id):
|
|||
del_form = DeleteTrophyForm()
|
||||
if request.method == "POST":
|
||||
if del_form.validate_on_submit():
|
||||
# TODO: Remove relationship with users that have the trophy
|
||||
db.session.delete(trophy)
|
||||
trophy.delete()
|
||||
db.session.commit()
|
||||
flash('Trophée supprimé', 'ok')
|
||||
return redirect(url_for('adm_trophies'))
|
||||
|
|
|
@ -20,21 +20,18 @@ def forum_index():
|
|||
@app.route('/forum/<forum:f>/', methods=['GET', 'POST'])
|
||||
@app.route('/forum/<forum:f>/p/<int:page>', methods=['GET', 'POST'])
|
||||
def forum_page(f, page=1):
|
||||
if not f.is_default_accessible() and not (
|
||||
current_user.is_authenticated and current_user.can_access_forum(f)):
|
||||
abort(403)
|
||||
|
||||
if current_user.is_authenticated:
|
||||
form = TopicCreationForm()
|
||||
else:
|
||||
form = AnonymousTopicCreationForm()
|
||||
|
||||
# TODO: do not hardcode name of news forums
|
||||
if form.validate_on_submit() and (
|
||||
# User can write anywhere
|
||||
(current_user.is_authenticated and current_user.priv('write-anywhere'))
|
||||
# Forum is news forum TODO: add good condition to check if it's news
|
||||
or ("/actus" in f.url and current_user.is_authenticated
|
||||
and current_user.priv('write-news'))
|
||||
# Forum is not news and is a leaf:
|
||||
or ("/actus" not in f.url and not f.sub_forums)) and (
|
||||
V5Config.ENABLE_GUEST_POST or current_user.is_authenticated):
|
||||
(V5Config.ENABLE_GUEST_POST and f.is_default_postable()) or \
|
||||
(current_user.is_authenticated and current_user.can_post_in_forum(f))):
|
||||
|
||||
# Manage author
|
||||
if current_user.is_authenticated:
|
||||
|
@ -83,4 +80,17 @@ def forum_page(f, page=1):
|
|||
topics = f.topics.order_by(Topic.date_created.desc()).paginate(
|
||||
page, Forum.TOPICS_PER_PAGE, True)
|
||||
|
||||
return render('/forum/forum.html', f=f, topics=topics, form=form)
|
||||
# Count comments; this direct request avoids performing one request for
|
||||
# each topic.thread.comments.count() in the view, which the database
|
||||
# doesn't really appreciate performance-wise.
|
||||
selection = " OR ".join(f"thread_id={t.thread.id}" for t in topics.items)
|
||||
selection = "WHERE " + selection if selection else ""
|
||||
|
||||
comment_counts = db.session.execute(f"""
|
||||
SELECT thread_id, COUNT(*) FROM comment {selection}
|
||||
GROUP BY thread_id
|
||||
""")
|
||||
comment_counts = dict(list(comment_counts))
|
||||
|
||||
return render('/forum/forum.html', f=f, topics=topics, form=form,
|
||||
comment_counts=comment_counts)
|
||||
|
|
|
@ -18,6 +18,10 @@ from datetime import datetime
|
|||
def forum_topic(f, page):
|
||||
t, page = page
|
||||
|
||||
if not f.is_default_accessible() and not (
|
||||
current_user.is_authenticated and current_user.can_access_forum(f)):
|
||||
abort(403)
|
||||
|
||||
# Quick n' dirty workaround to converters
|
||||
if f != t.forum:
|
||||
abort(404)
|
||||
|
@ -27,8 +31,10 @@ def forum_topic(f, page):
|
|||
else:
|
||||
form = AnonymousCommentForm()
|
||||
|
||||
if form.validate_on_submit() and \
|
||||
(V5Config.ENABLE_GUEST_POST or current_user.is_authenticated):
|
||||
if form.validate_on_submit() and (
|
||||
V5Config.ENABLE_GUEST_POST or \
|
||||
(current_user.is_authenticated and current_user.can_post_in_forum(f))):
|
||||
|
||||
# Manage author
|
||||
if current_user.is_authenticated:
|
||||
author = current_user
|
||||
|
@ -70,8 +76,8 @@ def forum_topic(f, page):
|
|||
if page == -1:
|
||||
page = (t.thread.comments.count() - 1) // Thread.COMMENTS_PER_PAGE + 1
|
||||
|
||||
comments = t.thread.comments.paginate(page,
|
||||
Thread.COMMENTS_PER_PAGE, True)
|
||||
comments = t.thread.comments.order_by(Comment.date_created.asc()) \
|
||||
.paginate(page, Thread.COMMENTS_PER_PAGE, True)
|
||||
|
||||
# Anti-necropost
|
||||
last_com = t.thread.comments.order_by(desc(Comment.date_modified)).first()
|
||||
|
|
|
@ -18,11 +18,7 @@ def poll_delete(poll_id):
|
|||
form = DeletePollForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
for a in poll.answers:
|
||||
db.session.delete(a)
|
||||
db.session.commit()
|
||||
|
||||
db.session.delete(poll)
|
||||
poll.delete()
|
||||
db.session.commit()
|
||||
|
||||
flash('Le sondage a été supprimé', 'info')
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
from app import app, db
|
||||
from app.models.attachment import Attachment
|
||||
from app.models.comment import Comment
|
||||
from app.models.forum import Forum
|
||||
from app.models.post import Post
|
||||
from app.models.program import Program
|
||||
from app.models.topic import Topic
|
||||
from app.models.user import Member
|
||||
from app.utils.render import render
|
||||
from app.utils.check_csrf import check_csrf
|
||||
from app.forms.forum import CommentEditForm, AnonymousCommentEditForm
|
||||
from app.forms.forum import CommentEditForm, AnonymousCommentEditForm, TopicEditForm
|
||||
from wtforms import BooleanField
|
||||
from urllib.parse import urlparse
|
||||
from flask import redirect, url_for, abort, request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
@app.route('/post/<int:postid>', methods=['GET','POST'])
|
||||
# TODO: Allow guest edit of posts
|
||||
@login_required
|
||||
def edit_post(postid):
|
||||
# TODO: Maybe not safe
|
||||
|
@ -17,42 +23,126 @@ def edit_post(postid):
|
|||
|
||||
p = Post.query.filter_by(id=postid).first_or_404()
|
||||
|
||||
# TODO: Check whether privileged user has access to board
|
||||
if p.author != current_user and not current_user.priv("edit-posts"):
|
||||
# Check permissions. TODO: Allow guests to edit their posts
|
||||
if current_user.is_anonymous or not current_user.can_edit_post(p):
|
||||
abort(403)
|
||||
|
||||
if p.type == "comment":
|
||||
form = CommentEditForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
p.text = form.message.data
|
||||
|
||||
if form.submit.data:
|
||||
db.session.add(p)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(referrer)
|
||||
|
||||
form.message.data = p.text
|
||||
return render('forum/edit_comment.html', comment=p, form=form)
|
||||
if isinstance(p, Comment):
|
||||
base = CommentEditForm
|
||||
comment = p
|
||||
elif isinstance(p, Topic):
|
||||
base = TopicEditForm
|
||||
comment = p.thread.top_comment
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
class TheForm(base):
|
||||
pass
|
||||
for a in comment.attachments:
|
||||
setattr(TheForm, f'a{a.id}', BooleanField(f'a{a.id}'))
|
||||
setattr(TheForm, 'attachment_list',
|
||||
{ f'a{a.id}': a for a in comment.attachments })
|
||||
form = TheForm()
|
||||
|
||||
if isinstance(p, Topic):
|
||||
forums = sorted(Forum.query.all(), key=lambda f: f.url)
|
||||
forums = [f for f in forums if current_user.can_post_in_forum(f)]
|
||||
form.forum.choices = [(f.url, f"{f.url}: {f.name}") for f in forums]
|
||||
|
||||
if form.validate_on_submit():
|
||||
comment.text = form.message.data
|
||||
|
||||
# Remove attachments
|
||||
for id, a in form.attachment_list.items():
|
||||
if form[id].data:
|
||||
a.delete()
|
||||
|
||||
# Add new attachments
|
||||
attachments = []
|
||||
for file in form.attachments.data:
|
||||
if file.filename != "":
|
||||
a = Attachment(file, comment)
|
||||
attachments.append((a, file))
|
||||
db.session.add(a)
|
||||
|
||||
db.session.add(comment)
|
||||
|
||||
if isinstance(p, Topic):
|
||||
p.title = form.title.data
|
||||
f = Forum.query.filter_by(url=form.forum.data).first_or_404()
|
||||
if current_user.can_post_in_forum(f):
|
||||
p.forum = f
|
||||
db.session.merge(p)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
for a, file in attachments:
|
||||
a.set_file(file)
|
||||
|
||||
# Determine topic URL now, in case forum was changed
|
||||
if isinstance(p, Topic):
|
||||
return redirect(url_for('forum_topic', f=p.forum, page=(p,1)))
|
||||
else:
|
||||
return redirect(referrer)
|
||||
|
||||
# Non-submitted form
|
||||
if isinstance(p, Comment):
|
||||
form.message.data = p.text
|
||||
return render('forum/edit_comment.html', comment=p, form=form)
|
||||
elif isinstance(p, Topic):
|
||||
form.message.data = p.thread.top_comment.text
|
||||
form.title.data = p.title
|
||||
form.forum.data = p.forum.url
|
||||
return render('forum/edit_topic.html', t=p, form=form)
|
||||
|
||||
@app.route('/post/supprimer/<int:postid>', methods=['GET','POST'])
|
||||
@login_required
|
||||
@check_csrf
|
||||
def delete_post(postid):
|
||||
next_page = request.referrer
|
||||
p = Post.query.filter_by(id=postid).first_or_404()
|
||||
xp = -1
|
||||
|
||||
# TODO: Check whether privileged user has access to board
|
||||
if p.author != current_user and not current_user.priv("delete-posts"):
|
||||
if current_user.is_anonymous or not current_user.can_delete_post(p):
|
||||
abort(403)
|
||||
|
||||
for a in p.attachments:
|
||||
a.delete_file()
|
||||
db.session.delete(a)
|
||||
# Users who need to have their trophies updated
|
||||
authors = set()
|
||||
|
||||
# When deleting topics, return to forum page
|
||||
if isinstance(p, Topic):
|
||||
next_page = url_for('forum_page', f=p.forum)
|
||||
xp = -2
|
||||
|
||||
for comment in p.thread.comments:
|
||||
if isinstance(comment.author, Member):
|
||||
comment.author.add_xp(-1)
|
||||
db.session.merge(comment.author)
|
||||
authors.add(comment.author)
|
||||
|
||||
if isinstance(p.author, Member):
|
||||
factor = 3 if request.args.get('penalty') == 'True' else 1
|
||||
p.author.add_xp(xp * factor)
|
||||
db.session.merge(p.author)
|
||||
authors.add(p.author)
|
||||
|
||||
p.delete()
|
||||
db.session.commit()
|
||||
|
||||
db.session.delete(p)
|
||||
db.session.commit()
|
||||
for author in authors:
|
||||
author.update_trophies("new-post")
|
||||
|
||||
return redirect(next_page)
|
||||
|
||||
@app.route('/post/entete/<int:postid>', methods=['GET'])
|
||||
@login_required
|
||||
@check_csrf
|
||||
def set_post_topcomment(postid):
|
||||
comment = Post.query.filter_by(id=postid).first_or_404()
|
||||
|
||||
if current_user.can_set_topcomment(comment):
|
||||
comment.thread.top_comment = comment
|
||||
db.session.add(comment.thread)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(request.referrer)
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
.form .avatar {
|
||||
width: 128px; height: 128px;
|
||||
}
|
||||
|
||||
.form .avatar + input[type="file"] {
|
||||
margin: 16px 0 0 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.form form > div:not(:last-child) {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form form label {
|
||||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form input {
|
||||
cursor: pointer; /* don't know why it is not a cursor by default */
|
||||
}
|
||||
|
||||
.form input[type='text'],
|
||||
.form input[type='email'],
|
||||
.form input[type='date'],
|
||||
.form input[type='password'],
|
||||
.form input[type='search'],
|
||||
.form textarea {
|
||||
display: block;
|
||||
width: 100%; padding: 6px 8px;
|
||||
border: 1px solid #c8c8c8;
|
||||
|
||||
/* Transitions when resizing with the mouse produces apparent lag */
|
||||
transition: all .15s ease, width 0s, height 0s;
|
||||
}
|
||||
.form input[type='text']:focus,
|
||||
.form input[type='email']:focus,
|
||||
.form input[type='date']:focus,
|
||||
.form input[type='password']:focus,
|
||||
.form input[type='search']:focus,
|
||||
.form textarea:focus {
|
||||
border-color: #91bfef;
|
||||
box-shadow: 0 0 0 3px rgba(87, 143, 228, 0.4);
|
||||
}
|
||||
|
||||
.form textarea {
|
||||
max-width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form input[type="submit"] {
|
||||
/*width: 20%;*/
|
||||
}
|
||||
|
||||
.form form .msgerror {
|
||||
color: red;
|
||||
font-weight: 400;
|
||||
margin-top: 5px;
|
||||
}
|
|
@ -1,51 +1,30 @@
|
|||
.container {
|
||||
margin-left: 110px;
|
||||
}
|
||||
/* Currently 3 screen sizes supported :
|
||||
- tiny: < 850px
|
||||
- small: < 1200px
|
||||
- normal: >= 1200px
|
||||
|
||||
Ex:
|
||||
@media screen and (max-width: @var)
|
||||
*/
|
||||
.container {
|
||||
margin-left: 110px;
|
||||
}
|
||||
@media screen and (max-width: 849px) {
|
||||
.container {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
section {
|
||||
width: 80%;
|
||||
margin: 20px auto 0 auto;
|
||||
margin: 20px auto 0 auto;
|
||||
}
|
||||
|
||||
section h1 {
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
font-family: Cantarell; font-weight: bold;
|
||||
font-size: 26px; color: #101010;
|
||||
@media screen and (max-width: 1449px) {
|
||||
section {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
section h2 {
|
||||
margin: 24px 0 16px 0;
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
font-family: Cantarell; font-weight: bold;
|
||||
font-size: 18px; color: #101010;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
section .avatar {
|
||||
display: block;
|
||||
width: 128px; height: 128px;
|
||||
}
|
||||
|
||||
|
||||
/* Some grid */
|
||||
.flex-grid {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.flex-grid > * {
|
||||
min-width: 250px;
|
||||
flex: auto;
|
||||
}
|
||||
/* Two columns */
|
||||
.flex-grid.fg2 > * {
|
||||
width: 50%;
|
||||
}
|
||||
/* Three columns */
|
||||
.flex-grid.fg3 > * {
|
||||
width: 33%;
|
||||
}
|
||||
/* Four columns */
|
||||
.flex-grid.fg4 > * {
|
||||
width: 25%;
|
||||
@media screen and (max-width: 1199px) {
|
||||
section {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
.editor div {
|
||||
display: flex; flex-direction: row;
|
||||
flex-wrap: wrap; align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.editor button {
|
||||
height: 25px; margin: 0 0px; padding: 0 3px;
|
||||
border: var(--border); border-radius: 2px;
|
||||
cursor: pointer;
|
||||
background: var(--background);
|
||||
}
|
||||
.editor button > img {
|
||||
opacity: .7;
|
||||
}
|
||||
.editor button:hover,
|
||||
.editor button:focus {
|
||||
border: var(--border-focused);
|
||||
}
|
||||
.editor button:hover > img,
|
||||
.editor button:focus > img {
|
||||
opacity: 1;
|
||||
}
|
|
@ -1,33 +1,32 @@
|
|||
/*
|
||||
flash overlay
|
||||
flash overlay
|
||||
*/
|
||||
|
||||
.flash {
|
||||
margin: 5px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 80%;
|
||||
font-size: 14px;
|
||||
border-bottom: 5px solid var(--info);
|
||||
border-radius: 1px;
|
||||
box-shadow: var(--shadow);
|
||||
margin: 5px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 80%;
|
||||
font-size: 14px;
|
||||
border-bottom: 5px solid var(--info);
|
||||
border-radius: 1px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.flash.info {
|
||||
border-color: var(--info);
|
||||
border-color: var(--info);
|
||||
}
|
||||
.flash.ok {
|
||||
border-color: var(--ok);
|
||||
border-color: var(--ok);
|
||||
}
|
||||
.flash.warning {
|
||||
border-color: var(--warn);
|
||||
border-color: var(--warn);
|
||||
}
|
||||
.flash.error {
|
||||
border-color: var(--error);
|
||||
border-color: var(--error);
|
||||
}
|
||||
.flash span {
|
||||
flex-grow: 1;
|
||||
margin: 15px 10px 10px 0;
|
||||
flex-grow: 1;
|
||||
margin: 15px 10px 10px 0;
|
||||
}
|
||||
.flash svg {
|
||||
margin: 15px 20px 10px 30px;
|
||||
margin: 15px 20px 10px 30px;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
/*
|
||||
Footer
|
||||
Footer
|
||||
*/
|
||||
|
||||
footer {
|
||||
margin: 20px 0 0 0; padding: 10px 10%;
|
||||
text-align: center; font-size: 11px; font-style: italic;
|
||||
background: var(--background); color: var(--text);
|
||||
border-top: var(--border);
|
||||
margin: 20px 0 0 0;
|
||||
padding: 10px 10%;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
border-top: var(--border);
|
||||
}
|
||||
footer p {
|
||||
margin: 3px 0;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
|
|
@ -1,142 +1,122 @@
|
|||
.form .avatar {
|
||||
width: 128px; height: 128px;
|
||||
/* Full-page forms */
|
||||
.form {
|
||||
/* anti-bots field */
|
||||
}
|
||||
|
||||
.form .avatar + input[type="file"] {
|
||||
margin: 16px 0 0 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.form form > div:not(:last-child):not(.editor-toolbar) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form form label,
|
||||
.trophies-panel p {
|
||||
display: inline-block;
|
||||
margin-bottom: 4px;
|
||||
.form form label {
|
||||
display: inline-block;
|
||||
margin: 0 5px 4px 0;
|
||||
}
|
||||
.form label + .desc {
|
||||
margin: 0 0 4px 0;
|
||||
.form form label + .desc {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 80%;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.form form .avatar {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
.form form .avatar + input[type="file"] {
|
||||
margin: 16px 0 0 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.form input[type='text'],
|
||||
.form input[type='email'],
|
||||
.form input[type='date'],
|
||||
.form input[type='password'],
|
||||
.form input[type='search'],
|
||||
.form textarea,
|
||||
.form select,
|
||||
.trophies-panel > div {
|
||||
display: block;
|
||||
width: 100%; padding: 6px 8px;
|
||||
background: var(--background); color: var(--text);
|
||||
border: var(--border);
|
||||
|
||||
/* Transitions when resizing with the mouse produces apparent lag */
|
||||
transition: all .15s ease, width 0s, height 0s;
|
||||
.form select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
border: var(--border);
|
||||
/* Transitions when resizing with the mouse produces apparent lag */
|
||||
transition: all 0.15s ease, width 0s, height 0s;
|
||||
}
|
||||
.form input[type='text']:focus,
|
||||
.form input[type='email']:focus,
|
||||
.form input[type='date']:focus,
|
||||
.form input[type='password']:focus,
|
||||
.form input[type='search']:focus,
|
||||
.form textarea:focus {
|
||||
border-color: var(--border-focused);
|
||||
box-shadow: 0 0 0 3px var(--shadow-focused);
|
||||
.form textarea:focus,
|
||||
.form select:focus {
|
||||
border-color: var(--border-focused);
|
||||
box-shadow: 0 0 0 3px var(--shadow-focused);
|
||||
}
|
||||
.form input[type='text']:focus-within,
|
||||
.form input[type='email']:focus-within,
|
||||
.form input[type='date']:focus-within,
|
||||
.form input[type='password']:focus-within,
|
||||
.form input[type='search']:focus-within,
|
||||
.form textarea:focus-within,
|
||||
.form select:focus-within {
|
||||
/* Override an annoying Firefox default */
|
||||
outline: none;
|
||||
}
|
||||
.form input[type='checkbox'],
|
||||
.form input[type='radio'] {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
margin: 0 4px 0 0;
|
||||
}
|
||||
|
||||
.form textarea {
|
||||
max-width: 100%;
|
||||
resize: vertical;
|
||||
max-width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
.form select {
|
||||
width: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.form progress.entropy {
|
||||
display: none; /* display with Js enabled */
|
||||
width: 100%; margin-top: 5px;
|
||||
background: var(--background);
|
||||
border: var(--border);
|
||||
}
|
||||
.form progress.entropy.low::-moz-progress-bar {
|
||||
background: var(--error);
|
||||
display: none;
|
||||
/* Display with Js enabled */
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
background: var(--background);
|
||||
border: var(--border);
|
||||
}
|
||||
.form progress.entropy.low::-moz-progress-bar,
|
||||
.form progress.entropy.low::-webkit-progress-bar {
|
||||
background: var(--error);
|
||||
}
|
||||
.form progress.entropy.medium::-moz-progress-bar {
|
||||
background: var(--warn);
|
||||
background: var(--error);
|
||||
}
|
||||
.form progress.entropy.medium::-moz-progress-bar,
|
||||
.form progress.entropy.medium::-webkit-progress-bar {
|
||||
background: var(--warn);
|
||||
}
|
||||
.form progress.entropy.high::-moz-progress-bar {
|
||||
background: var(--ok);
|
||||
background: var(--warn);
|
||||
}
|
||||
.form progress.entropy.high::-moz-progress-bar,
|
||||
.form progress.entropy.high::-webkit-progress-bar {
|
||||
background: var(--ok);
|
||||
background: var(--ok);
|
||||
}
|
||||
|
||||
.form input[type="checkbox"],
|
||||
.form input[type="radio"] {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.form input[type="submit"] {
|
||||
/*width: 20%;*/
|
||||
}
|
||||
|
||||
.form form .msgerror {
|
||||
color: var(--error);
|
||||
font-weight: 400;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.form .desc {
|
||||
font-size: 80%;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.form hr {
|
||||
height: 3px;
|
||||
border: var(--hr-border);
|
||||
border-width: 1px 0;
|
||||
margin: 24px 0;
|
||||
height: 3px;
|
||||
border: var(--hr-border);
|
||||
border-width: 1px 0;
|
||||
margin: 24px 0;
|
||||
}
|
||||
.trophies-panel label {
|
||||
margin-right: 5px;
|
||||
.form .msgerror {
|
||||
color: var(--error);
|
||||
font-weight: 400;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.trophies-panel p:first-child {
|
||||
margin-top: 0;
|
||||
.form .abfield {
|
||||
display: none;
|
||||
}
|
||||
.trophies-panel p label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Editor */
|
||||
|
||||
.editor textarea {
|
||||
font-family: monospace;
|
||||
height: 192px;
|
||||
}
|
||||
|
||||
/* Interactive filter forms */
|
||||
|
||||
.form.filter {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form.filter > p:first-child {
|
||||
font-size: 80%;
|
||||
color: gray;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.form.filter {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form.filter input {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.form.filter .syntax-explanation {
|
||||
font-size: 80%;
|
||||
color: gray;
|
||||
|
@ -149,10 +129,8 @@
|
|||
line-height: 20px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.form.filter .syntax-explanation li {
|
||||
}
|
||||
.form.filter .syntax-explanation code {
|
||||
background: rgba(0,0,0,.05);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 1px 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
|
|
@ -1,121 +1,149 @@
|
|||
/* Currently 3 screen sizes supported :
|
||||
- micro: < 500px
|
||||
- tiny: < 850px
|
||||
- small: < 1200px
|
||||
- normal: >= 1200px
|
||||
|
||||
Ex:
|
||||
@media screen and (max-width: @var)
|
||||
*/
|
||||
/* Fonts */
|
||||
|
||||
@font-face { font-family: NotoSans; src: url(../fonts/noto_sans.ttf); font-display: swap; }
|
||||
@font-face { font-family: Twemoji; src: url(../fonts/TwitterColorEmoji.ttf); font-display: swap; }
|
||||
@font-face { font-family: Cantarell; font-weight: normal; src: url(../fonts/Cantarell-Regular.otf); font-display: swap; }
|
||||
@font-face { font-family: Cantarell; font-weight: bold; src: url(../fonts/Cantarell-Bold.otf); font-display: swap; }
|
||||
|
||||
@font-face {
|
||||
font-family: NotoSans;
|
||||
src: url(../fonts/noto_sans.ttf);
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Twemoji;
|
||||
src: url(../fonts/TwitterColorEmoji.ttf);
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Cantarell;
|
||||
font-weight: normal;
|
||||
src: url(../fonts/Cantarell-Regular.otf);
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: Cantarell;
|
||||
font-weight: bold;
|
||||
src: url(../fonts/Cantarell-Bold.otf);
|
||||
font-display: swap;
|
||||
}
|
||||
/* Whole page */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
/* This transition value is replicated everywhere transitions are customized,
|
||||
make sure to track them when editing */
|
||||
transition: .15s ease;
|
||||
box-sizing: border-box;
|
||||
/* This transition value is replicated everywhere transitions are customized,
|
||||
make sure to track them when editing */
|
||||
transition: 0.15s ease;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--background); color: var(--text);
|
||||
font-family: 'DejaVu Sans', sans-serif;
|
||||
margin: 0;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
font-family: 'DejaVu Sans', sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
@media screen and (min-width: 1449px) {
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* General */
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--links);
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration: none;
|
||||
color: var(--links);
|
||||
}
|
||||
a:hover,
|
||||
a:focus {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
section p {
|
||||
line-height: 20px;
|
||||
word-wrap: anywhere;
|
||||
line-height: 20px;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
|
||||
section ul {
|
||||
line-height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
section h1 {
|
||||
margin-top: 0;
|
||||
border-bottom: var(--hr-border);
|
||||
font-family: Cantarell;
|
||||
font-weight: bold;
|
||||
font-size: 26px;
|
||||
color: var(--text-light);
|
||||
}
|
||||
section h2 {
|
||||
margin: 24px 0 16px 0;
|
||||
border-bottom: var(--hr-border);
|
||||
font-family: Cantarell;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
color: var(--text-light);
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
.button,
|
||||
input[type="button"],
|
||||
input[type="submit"] {
|
||||
padding: 6px 10px; border-radius: 2px;
|
||||
cursor: pointer;
|
||||
font-family: 'DejaVu Sans', sans-serif; font-weight: 400;
|
||||
border: 0;
|
||||
padding: 6px 10px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
font-family: 'DejaVu Sans', sans-serif;
|
||||
font-weight: 400;
|
||||
border: 0;
|
||||
}
|
||||
.button:hover,
|
||||
input[type="button"]:hover,
|
||||
input[type="submit"]:hover,
|
||||
.button:hover {
|
||||
text-decoration: none;
|
||||
.button:focus,
|
||||
input[type="button"]:focus,
|
||||
input[type="submit"]:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 1499px) {
|
||||
.profile-avatar {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
.profile-xp {
|
||||
height: 8px;
|
||||
min-width: 64px;
|
||||
}
|
||||
.profile-xp div {
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1199px) {
|
||||
.profile-points {
|
||||
display: none;
|
||||
}
|
||||
.profile-points-small {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Bootstrap-style rules
|
||||
*/
|
||||
/* Bootstrap-style rules */
|
||||
.flex {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bg-ok,
|
||||
.bg-ok {
|
||||
background: var(--ok);
|
||||
color: var(--ok-text);
|
||||
background: var(--ok);
|
||||
color: var(--ok-text);
|
||||
}
|
||||
.bg-ok:hover,
|
||||
.bg-ok:focus,
|
||||
.bg-ok:active {
|
||||
background: var(--ok-active);
|
||||
background: var(--ok-active);
|
||||
}
|
||||
|
||||
.bg-error,
|
||||
.bg-error {
|
||||
background: var(--error);
|
||||
color: var(--error-text);
|
||||
background: var(--error);
|
||||
color: var(--error-text);
|
||||
}
|
||||
.bg-error:hover,
|
||||
.bg-error:focus,
|
||||
.bg-error:active {
|
||||
background: var(--error-active);
|
||||
background: var(--error-active);
|
||||
}
|
||||
|
||||
.bg-warn {
|
||||
background: var(--warn);
|
||||
color: var(--warn-text);
|
||||
background: var(--warn);
|
||||
color: var(--warn-text);
|
||||
}
|
||||
.bg-warn:hover,
|
||||
.bg-warn:focus,
|
||||
.bg-warn:active {
|
||||
background: var(--warn-active);
|
||||
background: var(--warn-active);
|
||||
}
|
||||
.skip-to-content-link {
|
||||
height: 30px;
|
||||
left: 50%;
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
transform: translateY(-100%);
|
||||
transition: transform 0.3s;
|
||||
background: var(--links);
|
||||
color: var(--warn-text);
|
||||
border-radius: 1px;
|
||||
}
|
||||
.skip-to-content-link:focus {
|
||||
transform: translateY(0%);
|
||||
}
|
||||
|
|
|
@ -1,87 +1,107 @@
|
|||
/*
|
||||
header
|
||||
/* Currently 3 screen sizes supported :
|
||||
- micro: < 500px
|
||||
- tiny: < 850px
|
||||
- small: < 1200px
|
||||
- normal: >= 1200px
|
||||
|
||||
Ex:
|
||||
@media screen and (max-width: @var)
|
||||
*/
|
||||
|
||||
header {
|
||||
height: 50px; margin: 0; padding: 0 16px;
|
||||
background: var(--background); border-bottom: var(--border);
|
||||
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
flex-flow: row wrap;
|
||||
|
||||
/* When the search field occupies the rightmost position, the calculated
|
||||
position of the svg icon (on the right) might overflow from the header and
|
||||
induce horizontal scrolling. */
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 8px 16px;
|
||||
background: var(--background);
|
||||
border-bottom: var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-flow: row wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media screen and (max-width: 1199px) {
|
||||
#spotlight {
|
||||
display: none;
|
||||
}
|
||||
header input[type="search"] {
|
||||
width: 200px;
|
||||
}
|
||||
header .title {
|
||||
margin: 4px 0;
|
||||
}
|
||||
@media screen and (max-width: 849px) {
|
||||
header .form {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
header .title a {
|
||||
color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
header .title h1 {
|
||||
font-family: Cantarell; font-weight: bold; font-size: 18px;
|
||||
display: inline;
|
||||
font-family: Cantarell;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
header .spacer {
|
||||
flex: 1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
header .links {
|
||||
margin-left: 16px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
header svg {
|
||||
width: 24px; height: 24px; vertical-align: middle;
|
||||
transition: .15s ease;
|
||||
}
|
||||
header a:hover > svg, header a:focus > svg {
|
||||
fill: var(--text);
|
||||
}
|
||||
header a {
|
||||
fill: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
header .form {
|
||||
/* The search icon is draws inside the input field but its space is allocated
|
||||
on the right. Apply a negative margin to compensate this:
|
||||
-24px for the search icon
|
||||
-2px for the spacing between the search icon and the field */
|
||||
margin-right: -26px;
|
||||
margin-right: -26px;
|
||||
}
|
||||
@media screen and (max-width: 849px) {
|
||||
header .form {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
header .form input[type="search"] {
|
||||
display: inline-block; width: 250px;
|
||||
padding: 5px 35px 5px 10px;
|
||||
display: inline-block;
|
||||
width: 250px;
|
||||
padding: 5px 35px 5px 10px;
|
||||
}
|
||||
@media screen and (max-width: 1199px) {
|
||||
header .form input[type="search"] {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1449px) {
|
||||
header .form input[type="search"] {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
header .form input[type="search"]:focus ~ a {
|
||||
opacity: 1;
|
||||
}
|
||||
header .form input[type="search"] ~ a {
|
||||
position: relative; left: -33px;
|
||||
opacity: .7;
|
||||
position: relative;
|
||||
left: -33px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
header .form input[type="search"] ~ a > svg > path {
|
||||
fill: var(--text);
|
||||
fill: var(--text);
|
||||
}
|
||||
header .form input[type="search"] ~ a:hover,
|
||||
header .form input[type="search"]:focus ~ a {
|
||||
opacity: 1;
|
||||
header .form a {
|
||||
fill: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
#spotlight {
|
||||
margin-left: 16px;
|
||||
header .form a:hover > svg,
|
||||
header .form a:focus > svg {
|
||||
fill: var(--text);
|
||||
}
|
||||
#spotlight a {
|
||||
display: block;
|
||||
header .form svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
transition: 0.15s ease;
|
||||
}
|
||||
header #spotlight {
|
||||
margin-left: 16px;
|
||||
}
|
||||
@media screen and (max-width: 1199px) {
|
||||
header #spotlight {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
header #spotlight a {
|
||||
display: block;
|
||||
}
|
||||
#server-speed-warning {
|
||||
background: var(--warn);
|
||||
color: var(--warn-text);
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
padding: 4px;
|
||||
margin: 0 8px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
|
|
@ -1,134 +1,144 @@
|
|||
/*
|
||||
home-title
|
||||
home-title
|
||||
*/
|
||||
|
||||
.home-title {
|
||||
margin: 20px 0; padding: 10px 5%;
|
||||
background: #bf1c11; box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
|
||||
border-top: 10px solid #ab170c;
|
||||
margin: 20px 0;
|
||||
padding: 10px 5%;
|
||||
background: #bf1c11;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3);
|
||||
border-top: 10px solid #ab170c;
|
||||
}
|
||||
|
||||
.home-title h1 {
|
||||
margin-top: 0;
|
||||
color: #ffffff; border-color: #ffffff;
|
||||
.home-title h1 {
|
||||
margin-top: 0;
|
||||
color: #ffffff;
|
||||
border-color: #ffffff;
|
||||
}
|
||||
|
||||
.home-title p {
|
||||
margin-bottom: 0; text-align: justify;
|
||||
color: #ffffff;
|
||||
margin-bottom: 0;
|
||||
text-align: justify;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.home-title a {
|
||||
color: inherit; text-decoration: underline;
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
pinned-content
|
||||
pinned-content
|
||||
*/
|
||||
|
||||
.home-pinned-content > div {
|
||||
display: flex; justify-content: space-between;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.home-pinned-content article {
|
||||
flex-grow: 1; margin: 0 1px; padding: 0;
|
||||
position: relative;
|
||||
max-width: 250px; overflow: hidden;
|
||||
}
|
||||
|
||||
.home-pinned-content a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.home-pinned-content img {
|
||||
width: 100%; filter: blur(0px);
|
||||
}
|
||||
|
||||
.home-pinned-content article div {
|
||||
position: absolute; bottom: 0; z-index: 3;
|
||||
width: 90%; margin: 0;
|
||||
padding: 30px 5% 10px 5%;
|
||||
color: #ffffff; text-shadow: 1px 1px 0 rgba(0,0,0,.6);
|
||||
background-image: linear-gradient(180deg,transparent 0,rgba(0,0,0,.7) 40px,rgba(0,0,0,.8));
|
||||
}
|
||||
|
||||
.home-pinned-content h2 {
|
||||
display: block; margin: 5px 0;
|
||||
font-size: 18px; font-family: NotoSans; font-weight: 200;
|
||||
line-height: 20px;
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
font-size: 18px;
|
||||
font-family: NotoSans;
|
||||
font-weight: 200;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
home-articles
|
||||
*/
|
||||
|
||||
.home-articles {
|
||||
display: flex; justify-content: space-between;
|
||||
.home-pinned-content a {
|
||||
display: block;
|
||||
}
|
||||
.home-articles > div {
|
||||
flex-grow: 1; max-width: 48%;
|
||||
}
|
||||
.home-articles h1 {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.home-articles h1 a {
|
||||
padding: 0;
|
||||
font-family: NotoSans; font-size: 16px;
|
||||
font-weight: 400; color: /*#015078*/ /*#bf1c11*/ #234d5f;
|
||||
}
|
||||
|
||||
.home-articles article {
|
||||
padding: 10px; margin: 10px 0; display: flex; align-items: center;
|
||||
background: #ffffff; border: 1px solid rgba(0, 0, 0, .2);
|
||||
}
|
||||
.home-articles article > img {
|
||||
float: left; margin-right: 10px; flex-shrink: 0;
|
||||
}
|
||||
.home-articles article > img.screeshot {
|
||||
width: 128px; height: 64px;
|
||||
}
|
||||
.home-articles article > div {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.home-articles article h3 {
|
||||
margin: 0;
|
||||
color: #424242; font-weight: normal;
|
||||
}
|
||||
.home-articles p {
|
||||
margin: 5px 0;
|
||||
text-align: justify;
|
||||
color: #808080;
|
||||
}
|
||||
.home-articles .metadata {
|
||||
margin: 0;
|
||||
color: #22292c;
|
||||
}
|
||||
.home-articles .metadata a {
|
||||
color: #22292c; font-weight: 400; font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
hover rules
|
||||
*/
|
||||
|
||||
.home-pinned-content a:hover img,
|
||||
.home-pinned-content a:focus img {
|
||||
filter: blur(3px);
|
||||
filter: blur(3px);
|
||||
}
|
||||
.home-pinned-content a:hover div,
|
||||
.home-pinned-content a:focus div {
|
||||
padding: 200px 5% 10px 5%;
|
||||
background-image: linear-gradient(180deg,transparent 0,rgba(0,0,0,.7) 40px,rgba(0,0,0,.8));
|
||||
padding: 200px 5% 10px 5%;
|
||||
background-image: linear-gradient(180deg, transparent 0, rgba(0, 0, 0, 0.7) 40px, rgba(0, 0, 0, 0.8));
|
||||
}
|
||||
.home-pinned-content img {
|
||||
width: 100%;
|
||||
filter: blur(0px);
|
||||
}
|
||||
.home-pinned-content article {
|
||||
flex-grow: 1;
|
||||
margin: 0 1px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.home-pinned-content article div {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
width: 90%;
|
||||
margin: 0;
|
||||
padding: 30px 5% 10px 5%;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.6);
|
||||
background-image: linear-gradient(180deg, transparent 0, rgba(0, 0, 0, 0.7) 40px, rgba(0, 0, 0, 0.8));
|
||||
}
|
||||
/*
|
||||
home-articles
|
||||
*/
|
||||
.home-articles {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.home-articles > div {
|
||||
flex-grow: 1;
|
||||
max-width: 48%;
|
||||
}
|
||||
.home-articles h1 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.home-articles h1 a {
|
||||
padding: 0;
|
||||
font-family: NotoSans;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: #234d5f;
|
||||
}
|
||||
|
||||
.home-articles h1 a:hover,
|
||||
.home-articles h1 a:focus {
|
||||
padding-right: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.home-articles p {
|
||||
margin: 5px 0;
|
||||
text-align: justify;
|
||||
color: #808080;
|
||||
}
|
||||
.home-articles article {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.home-articles article > img {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.home-articles article > img.screeshot {
|
||||
width: 128px;
|
||||
height: 64px;
|
||||
}
|
||||
.home-articles article > div {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.home-articles article h3 {
|
||||
margin: 0;
|
||||
color: #424242;
|
||||
font-weight: normal;
|
||||
}
|
||||
.home-articles article a:hover,
|
||||
.home-articles article a:focus {
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.home-articles .metadata {
|
||||
margin: 0;
|
||||
color: #22292c;
|
||||
}
|
||||
.home-articles .metadata a {
|
||||
color: #22292c;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
|
@ -1,223 +0,0 @@
|
|||
/* Whole page */
|
||||
|
||||
.light-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Menu */
|
||||
|
||||
#light-menu {
|
||||
position: unset;
|
||||
display: flex; flex-direction: row; align-items: center;
|
||||
width: 100%; height: 60px;
|
||||
overflow-x: auto; overflow-y: hidden;
|
||||
}
|
||||
|
||||
#logo {
|
||||
width: auto; height: 100%; margin-bottom: 0;
|
||||
}
|
||||
#logo img {
|
||||
width: 60px; height: inherit;
|
||||
margin-bottom: -4.5px;
|
||||
}
|
||||
|
||||
#light-menu li {
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; flex-grow: 1;
|
||||
height: 100%;
|
||||
padding: 0 2px;
|
||||
}
|
||||
#light-menu li > a {
|
||||
cursor: pointer; margin: 0;
|
||||
}
|
||||
#light-menu li > a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
#light-menu li > a > svg {
|
||||
width: 20px;
|
||||
}
|
||||
#light-menu li > a > div {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
}
|
||||
#light-menu li:not(.opened) > a:hover::after,
|
||||
#light-menu li:not(.opened) > a:focus::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
#light-menu li span[notifications]:not([notifications="0"])::before {
|
||||
content: attr(notifications);
|
||||
display: inline-block; margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
padding: 0 5px 0 4px; border-radius: 5px;
|
||||
font-family: NotoSans;
|
||||
background: #ffffff; color: #000000;
|
||||
}
|
||||
|
||||
|
||||
#menu {
|
||||
width: 100%; height: 0; overflow-x: hidden;
|
||||
font-family: NotoSans; font-size: 12px;
|
||||
transition: .1s ease;
|
||||
position: unset;
|
||||
left: unset;
|
||||
}
|
||||
#menu.opened {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
left: unset;
|
||||
}
|
||||
|
||||
#menu > div {
|
||||
width: 100%;
|
||||
}
|
||||
#menu h2 {
|
||||
font-size: 15px;
|
||||
}
|
||||
#menu h2 > svg {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
#menu span {
|
||||
display: block;
|
||||
color: #b8b8b8;
|
||||
font-size: 10px;
|
||||
}
|
||||
#menu span > a {
|
||||
display: inline;
|
||||
margin: 0; font-style: normal;
|
||||
font-size: 12px;
|
||||
}
|
||||
#menu ul {
|
||||
list-style: none;
|
||||
margin: 10px 0; padding: 0;
|
||||
line-height: 20px;
|
||||
color: #b8b8b8;
|
||||
}
|
||||
#menu li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
#light-menu, #spacer-menu {
|
||||
height: 40px;
|
||||
}
|
||||
#logo img {
|
||||
width: 40px;
|
||||
}
|
||||
#light-menu li > a > div {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#menu form input {
|
||||
display: block;
|
||||
margin: 5px 15px; padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
transition: .15s ease;
|
||||
}
|
||||
#menu form label {
|
||||
float: left; margin-right: 10px;
|
||||
}
|
||||
#menu form input:first-child {
|
||||
margin-bottom: 0; border-bottom: none;
|
||||
border-top-left-radius: 5px;
|
||||
-webkit-border-top-left-radius: 5px;
|
||||
-moz-border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
-webkit-border-top-right-radius: 5px;
|
||||
-moz-border-top-right-radius: 5px;
|
||||
}
|
||||
#menu form input:nth-child(2) {
|
||||
margin-top: 0; border-top: 1px solid #dddddd;
|
||||
border-bottom-left-radius: 5px;
|
||||
-webkit-border-bottom-left-radius: 5px;
|
||||
-moz-border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
-webkit-border-bottom-right-radius: 5px;
|
||||
-moz-border-bottom-right-radius: 5px;
|
||||
}
|
||||
#menu form a {
|
||||
display: block; margin-left: 15px;
|
||||
}
|
||||
|
||||
|
||||
/* Header */
|
||||
|
||||
header {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* Homepage */
|
||||
|
||||
#shoutbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section {
|
||||
width: unset;
|
||||
margin: 16px;
|
||||
}
|
||||
.home-title {
|
||||
padding: 10px;
|
||||
}
|
||||
.home-title p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.home-pinned-content {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.home-pinned-content article {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.home-pinned-content article > a {
|
||||
width: 100%;
|
||||
display: flex; align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
.home-pinned-content img {
|
||||
flex-shrink: 0;
|
||||
width: 100px; height: 100px;
|
||||
}
|
||||
.home-pinned-content article div {
|
||||
flex-grow: 1; margin-left: 10px;
|
||||
}
|
||||
.home-pinned-content h2 {
|
||||
margin: 0; color: #242424;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.home-pinned-content span {
|
||||
color: #000000; font-size: 14px;
|
||||
}
|
||||
|
||||
.home-articles > div {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.home-articles article {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.home-articles article > img {
|
||||
flex-shrink: 0; width: 128px; height: 64px;
|
||||
}
|
||||
.home-articles article > div {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.home-articles h1 > a {
|
||||
font-size: 13px; color: #666666;
|
||||
}
|
||||
.home-articles p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
/* Notifications */
|
||||
|
||||
.alert {
|
||||
display: none;
|
||||
}
|
|
@ -1,183 +1,326 @@
|
|||
/* Currently 3 screen sizes supported :
|
||||
- micro: < 500px
|
||||
- tiny: < 850px
|
||||
- small: < 1200px
|
||||
- normal: >= 1200px
|
||||
|
||||
Ex:
|
||||
@media screen and (max-width: @var)
|
||||
*/
|
||||
nav a {
|
||||
opacity: .8;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
}
|
||||
nav a:hover,
|
||||
nav a:focus {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/* Menu */
|
||||
|
||||
#light-menu {
|
||||
position: fixed; z-index: 10;
|
||||
list-style: none;
|
||||
width: 110px;
|
||||
height: 100%; overflow-y: auto;
|
||||
margin: 0; padding: 0;
|
||||
text-indent: 0;
|
||||
background: var(--background); box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
#logo {
|
||||
position: relative; display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
opacity: 1;
|
||||
background: var(--logo-bg);
|
||||
transition: .15s ease;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
opacity: 1;
|
||||
background: var(--logo-bg);
|
||||
transition: 0.15s ease;
|
||||
}
|
||||
#logo img {
|
||||
display: block; height: 65px;
|
||||
margin: 0 auto; padding: 0;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, .0));
|
||||
transition: filter .15s ease;
|
||||
@media screen and (max-width: 849px) {
|
||||
#logo {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
#logo:hover,
|
||||
#logo:focus {
|
||||
background: var(--logo-active);
|
||||
background: var(--logo-active);
|
||||
}
|
||||
#logo:hover img,
|
||||
#logo:focus img {
|
||||
filter: drop-shadow(var(--logo-shadow));
|
||||
filter: drop-shadow(var(--logo-shadow));
|
||||
}
|
||||
#logo img {
|
||||
display: block;
|
||||
height: 65px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0));
|
||||
transition: filter 0.15s ease;
|
||||
}
|
||||
@media screen and (max-width: 1199px) {
|
||||
#logo img {
|
||||
width: 60px;
|
||||
height: inherit;
|
||||
margin-bottom: -4.5px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 499px) {
|
||||
#logo img {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
#light-menu {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
list-style: none;
|
||||
width: 110px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-indent: 0;
|
||||
font-size: 13px;
|
||||
background: var(--background);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
@media screen and (max-width: 849px) {
|
||||
#light-menu {
|
||||
position: unset;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 499px) {
|
||||
#light-menu {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
#light-menu li {
|
||||
width: 100%;
|
||||
color: var(--text);
|
||||
width: 100%;
|
||||
color: var(--text);
|
||||
}
|
||||
@media screen and (max-width: 849px) {
|
||||
#light-menu li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
padding: 0 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
#light-menu li > a {
|
||||
display: flex; flex-direction: column; flex-grow: 0;
|
||||
align-items: center; justify-content: center;
|
||||
width: 100%; height: 100%;
|
||||
margin: 20px 0;
|
||||
color: var(--text);
|
||||
transition: opacity .15s ease; /* because Chrome sucks */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 20px 0;
|
||||
color: var(--text);
|
||||
transition: opacity 0.15s ease;
|
||||
/* because Chrome sucks */
|
||||
/* Avatar */
|
||||
}
|
||||
|
||||
#light-menu li > a > img {
|
||||
display: block; width: 60px; flex-shrink: 0; flex-grow: 0;
|
||||
margin: 0 7px 5px 7px;
|
||||
border-radius: 10%;
|
||||
display: block;
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
margin: 0 7px 5px 7px;
|
||||
border-radius: 10%;
|
||||
}
|
||||
@media screen and (max-width: 849px) {
|
||||
#light-menu li > a > img {
|
||||
width: 45px;
|
||||
margin: 0;
|
||||
}
|
||||
#light-menu li > a > img ~ div {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 499px) {
|
||||
#light-menu li > a > img {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
#light-menu li > a > svg {
|
||||
display: block; width: 25px; flex-shrink: 0; flex-grow: 0;
|
||||
margin: 0 7px;
|
||||
display: block;
|
||||
width: 25px;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
margin: 0 7px;
|
||||
}
|
||||
#light-menu li > a > svg > path {
|
||||
fill: var(--icons);
|
||||
fill: var(--icons);
|
||||
}
|
||||
|
||||
#light-menu li div {
|
||||
/*flex-grow: 1;*/
|
||||
@media screen and (max-width: 499px) {
|
||||
#light-menu li > a > div {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Overlay */
|
||||
#menu {
|
||||
position: fixed; z-index: 5;
|
||||
left: -190px; width: 300px; /* default: left-to-right animation */
|
||||
height: 100%; overflow-x: hidden; overflow-y: auto;
|
||||
background: var(--background); color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
transition: .15s ease;
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
left: -190px;
|
||||
width: 300px;
|
||||
/* default: left-to-right animation */
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
font-size: 13px;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
transition: 0.15s ease;
|
||||
/* Set class="scroll-animation" to menu to apply scroll animation */
|
||||
/* Login form */
|
||||
}
|
||||
@media screen and (max-width: 849px) {
|
||||
#menu {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow-x: hidden;
|
||||
font-family: NotoSans;
|
||||
transition: 0.1s ease;
|
||||
position: unset;
|
||||
left: unset;
|
||||
}
|
||||
}
|
||||
#menu.opened {
|
||||
left: 110px;
|
||||
left: 110px;
|
||||
}
|
||||
@media screen and (max-width: 849px) {
|
||||
#menu.opened {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
left: unset;
|
||||
}
|
||||
}
|
||||
|
||||
/* Just apply class="scroll-animation" to menu to change to scroll animation */
|
||||
#menu.scroll-animation {
|
||||
left: 110px; width: 0;
|
||||
left: 110px;
|
||||
width: 0;
|
||||
}
|
||||
#menu.scroll-animation.opened {
|
||||
width: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
|
||||
#menu > div {
|
||||
width: 300px;
|
||||
padding: 16px;
|
||||
display: none;
|
||||
width: 300px;
|
||||
padding: 16px;
|
||||
display: none;
|
||||
}
|
||||
@media screen and (max-width: 849px) {
|
||||
#menu > div {
|
||||
width: 100%;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
}
|
||||
#menu > div.opened {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#menu h2 {
|
||||
margin: 0 0 20px 0;
|
||||
font-family: Cantarell; font-weight: bold; font-size: 18px;
|
||||
color: var(--text);
|
||||
display: flex; align-items: center;
|
||||
margin: 0 0 20px 0;
|
||||
font-family: Cantarell;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@media screen and (max-width: 499px) {
|
||||
#menu h2 {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
#menu h2 a {
|
||||
margin: 0;
|
||||
font-size: inherit; opacity: inherit;
|
||||
}
|
||||
#menu h2 > svg {
|
||||
width: 32px; vertical-align: middle; margin-right: 8px;
|
||||
}
|
||||
#menu h2 img {
|
||||
height: 48px; vertical-align: middle; margin-right: 10px;
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
opacity: inherit;
|
||||
}
|
||||
#menu h2 a:hover,
|
||||
#menu h2 a:focus {
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
#menu h2 > svg {
|
||||
width: 32px;
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
}
|
||||
@media screen and (max-width: 499px) {
|
||||
#menu h2 > svg {
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
#menu h2 img {
|
||||
height: 48px;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#menu h3 {
|
||||
margin: 16px 0;
|
||||
font-family: Cantarell; font-weight: bold; font-size: 15px;
|
||||
color: var(--text);
|
||||
margin: 16px 0;
|
||||
font-family: Cantarell;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
color: var(--text);
|
||||
}
|
||||
#menu hr {
|
||||
margin: 15px 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin: 15px 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
#menu ul {
|
||||
margin: 0; padding: 0; list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
#menu a,
|
||||
#menu li {
|
||||
display: block; margin: 10px 0;
|
||||
color: var(--text);
|
||||
transition: opacity .15s ease;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
color: var(--text);
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
#menu li > a {
|
||||
display: inline;
|
||||
margin: 0; font-style: normal;
|
||||
font-size: 13px;
|
||||
display: inline;
|
||||
margin: 0;
|
||||
font-style: normal;
|
||||
font-size: 13px;
|
||||
}
|
||||
#menu a > img {
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
}
|
||||
#menu a > svg {
|
||||
width: 20px; height: 20px; vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#menu form {
|
||||
padding: 0 8%;
|
||||
padding: 0 8%;
|
||||
}
|
||||
@media screen and (max-width: 849px) {
|
||||
#menu form {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
#menu form input[type="text"],
|
||||
#menu form input[type="password"] {
|
||||
margin: 8px 0; padding: 5px 2%;
|
||||
font-size: 14px; color: inherit;
|
||||
border: var(--input-border);
|
||||
background: var(--input-bg); color: var(--input-text); opacity: .8;
|
||||
margin: 8px 0;
|
||||
padding: 5px 2%;
|
||||
font-size: 14px;
|
||||
border: var(--input-border);
|
||||
background: var(--input-bg);
|
||||
color: var(--input-text);
|
||||
opacity: 0.8;
|
||||
}
|
||||
#menu form input[type="text"]:focus,
|
||||
#menu form input[type="password"]:focus {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
#menu form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin: 8px 0 5px 0;
|
||||
width: 100%;
|
||||
margin: 8px 0 5px 0;
|
||||
}
|
||||
#menu form label {
|
||||
font-size: 13px; opacity: .8;
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos { padding: 0 5px; }
|
||||
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
|
@ -0,0 +1,74 @@
|
|||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: #666666; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: #666666; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.codehilite .hll { background-color: #ffffcc }
|
||||
.codehilite { background: #f0f0f0; }
|
||||
.codehilite .c { color: #60a0b0; font-style: italic } /* Comment */
|
||||
.codehilite .err { border: 1px solid #FF0000 } /* Error */
|
||||
.codehilite .k { color: #007020; font-weight: bold } /* Keyword */
|
||||
.codehilite .o { color: #666666 } /* Operator */
|
||||
.codehilite .ch { color: #60a0b0; font-style: italic } /* Comment.Hashbang */
|
||||
.codehilite .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */
|
||||
.codehilite .cp { color: #007020 } /* Comment.Preproc */
|
||||
.codehilite .cpf { color: #60a0b0; font-style: italic } /* Comment.PreprocFile */
|
||||
.codehilite .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */
|
||||
.codehilite .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */
|
||||
.codehilite .gd { color: #A00000 } /* Generic.Deleted */
|
||||
.codehilite .ge { font-style: italic } /* Generic.Emph */
|
||||
.codehilite .gr { color: #FF0000 } /* Generic.Error */
|
||||
.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.codehilite .gi { color: #00A000 } /* Generic.Inserted */
|
||||
.codehilite .go { color: #888888 } /* Generic.Output */
|
||||
.codehilite .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */
|
||||
.codehilite .gs { font-weight: bold } /* Generic.Strong */
|
||||
.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.codehilite .gt { color: #0044DD } /* Generic.Traceback */
|
||||
.codehilite .kc { color: #007020; font-weight: bold } /* Keyword.Constant */
|
||||
.codehilite .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */
|
||||
.codehilite .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */
|
||||
.codehilite .kp { color: #007020 } /* Keyword.Pseudo */
|
||||
.codehilite .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */
|
||||
.codehilite .kt { color: #902000 } /* Keyword.Type */
|
||||
.codehilite .m { color: #40a070 } /* Literal.Number */
|
||||
.codehilite .s { color: #4070a0 } /* Literal.String */
|
||||
.codehilite .na { color: #4070a0 } /* Name.Attribute */
|
||||
.codehilite .nb { color: #007020 } /* Name.Builtin */
|
||||
.codehilite .nc { color: #0e84b5; font-weight: bold } /* Name.Class */
|
||||
.codehilite .no { color: #60add5 } /* Name.Constant */
|
||||
.codehilite .nd { color: #555555; font-weight: bold } /* Name.Decorator */
|
||||
.codehilite .ni { color: #d55537; font-weight: bold } /* Name.Entity */
|
||||
.codehilite .ne { color: #007020 } /* Name.Exception */
|
||||
.codehilite .nf { color: #06287e } /* Name.Function */
|
||||
.codehilite .nl { color: #002070; font-weight: bold } /* Name.Label */
|
||||
.codehilite .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */
|
||||
.codehilite .nt { color: #062873; font-weight: bold } /* Name.Tag */
|
||||
.codehilite .nv { color: #bb60d5 } /* Name.Variable */
|
||||
.codehilite .ow { color: #007020; font-weight: bold } /* Operator.Word */
|
||||
.codehilite .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.codehilite .mb { color: #40a070 } /* Literal.Number.Bin */
|
||||
.codehilite .mf { color: #40a070 } /* Literal.Number.Float */
|
||||
.codehilite .mh { color: #40a070 } /* Literal.Number.Hex */
|
||||
.codehilite .mi { color: #40a070 } /* Literal.Number.Integer */
|
||||
.codehilite .mo { color: #40a070 } /* Literal.Number.Oct */
|
||||
.codehilite .sa { color: #4070a0 } /* Literal.String.Affix */
|
||||
.codehilite .sb { color: #4070a0 } /* Literal.String.Backtick */
|
||||
.codehilite .sc { color: #4070a0 } /* Literal.String.Char */
|
||||
.codehilite .dl { color: #4070a0 } /* Literal.String.Delimiter */
|
||||
.codehilite .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */
|
||||
.codehilite .s2 { color: #4070a0 } /* Literal.String.Double */
|
||||
.codehilite .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */
|
||||
.codehilite .sh { color: #4070a0 } /* Literal.String.Heredoc */
|
||||
.codehilite .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */
|
||||
.codehilite .sx { color: #c65d09 } /* Literal.String.Other */
|
||||
.codehilite .sr { color: #235388 } /* Literal.String.Regex */
|
||||
.codehilite .s1 { color: #4070a0 } /* Literal.String.Single */
|
||||
.codehilite .ss { color: #517918 } /* Literal.String.Symbol */
|
||||
.codehilite .bp { color: #007020 } /* Name.Builtin.Pseudo */
|
||||
.codehilite .fm { color: #06287e } /* Name.Function.Magic */
|
||||
.codehilite .vc { color: #bb60d5 } /* Name.Variable.Class */
|
||||
.codehilite .vg { color: #bb60d5 } /* Name.Variable.Global */
|
||||
.codehilite .vi { color: #bb60d5 } /* Name.Variable.Instance */
|
||||
.codehilite .vm { color: #bb60d5 } /* Name.Variable.Magic */
|
||||
.codehilite .il { color: #40a070 } /* Literal.Number.Integer.Long */
|
|
@ -0,0 +1,82 @@
|
|||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: #37474F; background-color: #263238; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: #37474F; background-color: #263238; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #607A86; background-color: #263238; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #607A86; background-color: #263238; padding-left: 5px; padding-right: 5px; }
|
||||
.codehilite .hll { background-color: #2C3B41 }
|
||||
.codehilite { background: #263238; color: #EEFFFF }
|
||||
.codehilite .c { color: #546E7A; font-style: italic } /* Comment */
|
||||
.codehilite .err { color: #FF5370 } /* Error */
|
||||
.codehilite .esc { color: #89DDFF } /* Escape */
|
||||
.codehilite .g { color: #EEFFFF } /* Generic */
|
||||
.codehilite .k { color: #BB80B3 } /* Keyword */
|
||||
.codehilite .l { color: #C3E88D } /* Literal */
|
||||
.codehilite .n { color: #EEFFFF } /* Name */
|
||||
.codehilite .o { color: #89DDFF } /* Operator */
|
||||
.codehilite .p { color: #89DDFF } /* Punctuation */
|
||||
.codehilite .ch { color: #546E7A; font-style: italic } /* Comment.Hashbang */
|
||||
.codehilite .cm { color: #546E7A; font-style: italic } /* Comment.Multiline */
|
||||
.codehilite .cp { color: #546E7A; font-style: italic } /* Comment.Preproc */
|
||||
.codehilite .cpf { color: #546E7A; font-style: italic } /* Comment.PreprocFile */
|
||||
.codehilite .c1 { color: #546E7A; font-style: italic } /* Comment.Single */
|
||||
.codehilite .cs { color: #546E7A; font-style: italic } /* Comment.Special */
|
||||
.codehilite .gd { color: #FF5370 } /* Generic.Deleted */
|
||||
.codehilite .ge { color: #89DDFF } /* Generic.Emph */
|
||||
.codehilite .gr { color: #FF5370 } /* Generic.Error */
|
||||
.codehilite .gh { color: #C3E88D } /* Generic.Heading */
|
||||
.codehilite .gi { color: #C3E88D } /* Generic.Inserted */
|
||||
.codehilite .go { color: #546E7A } /* Generic.Output */
|
||||
.codehilite .gp { color: #FFCB6B } /* Generic.Prompt */
|
||||
.codehilite .gs { color: #FF5370 } /* Generic.Strong */
|
||||
.codehilite .gu { color: #89DDFF } /* Generic.Subheading */
|
||||
.codehilite .gt { color: #FF5370 } /* Generic.Traceback */
|
||||
.codehilite .kc { color: #89DDFF } /* Keyword.Constant */
|
||||
.codehilite .kd { color: #BB80B3 } /* Keyword.Declaration */
|
||||
.codehilite .kn { color: #89DDFF; font-style: italic } /* Keyword.Namespace */
|
||||
.codehilite .kp { color: #89DDFF } /* Keyword.Pseudo */
|
||||
.codehilite .kr { color: #BB80B3 } /* Keyword.Reserved */
|
||||
.codehilite .kt { color: #BB80B3 } /* Keyword.Type */
|
||||
.codehilite .ld { color: #C3E88D } /* Literal.Date */
|
||||
.codehilite .m { color: #F78C6C } /* Literal.Number */
|
||||
.codehilite .s { color: #C3E88D } /* Literal.String */
|
||||
.codehilite .na { color: #BB80B3 } /* Name.Attribute */
|
||||
.codehilite .nb { color: #82AAFF } /* Name.Builtin */
|
||||
.codehilite .nc { color: #FFCB6B } /* Name.Class */
|
||||
.codehilite .no { color: #EEFFFF } /* Name.Constant */
|
||||
.codehilite .nd { color: #82AAFF } /* Name.Decorator */
|
||||
.codehilite .ni { color: #89DDFF } /* Name.Entity */
|
||||
.codehilite .ne { color: #FFCB6B } /* Name.Exception */
|
||||
.codehilite .nf { color: #82AAFF } /* Name.Function */
|
||||
.codehilite .nl { color: #82AAFF } /* Name.Label */
|
||||
.codehilite .nn { color: #FFCB6B } /* Name.Namespace */
|
||||
.codehilite .nx { color: #EEFFFF } /* Name.Other */
|
||||
.codehilite .py { color: #FFCB6B } /* Name.Property */
|
||||
.codehilite .nt { color: #FF5370 } /* Name.Tag */
|
||||
.codehilite .nv { color: #89DDFF } /* Name.Variable */
|
||||
.codehilite .ow { color: #89DDFF; font-style: italic } /* Operator.Word */
|
||||
.codehilite .w { color: #EEFFFF } /* Text.Whitespace */
|
||||
.codehilite .mb { color: #F78C6C } /* Literal.Number.Bin */
|
||||
.codehilite .mf { color: #F78C6C } /* Literal.Number.Float */
|
||||
.codehilite .mh { color: #F78C6C } /* Literal.Number.Hex */
|
||||
.codehilite .mi { color: #F78C6C } /* Literal.Number.Integer */
|
||||
.codehilite .mo { color: #F78C6C } /* Literal.Number.Oct */
|
||||
.codehilite .sa { color: #BB80B3 } /* Literal.String.Affix */
|
||||
.codehilite .sb { color: #C3E88D } /* Literal.String.Backtick */
|
||||
.codehilite .sc { color: #C3E88D } /* Literal.String.Char */
|
||||
.codehilite .dl { color: #EEFFFF } /* Literal.String.Delimiter */
|
||||
.codehilite .sd { color: #546E7A; font-style: italic } /* Literal.String.Doc */
|
||||
.codehilite .s2 { color: #C3E88D } /* Literal.String.Double */
|
||||
.codehilite .se { color: #EEFFFF } /* Literal.String.Escape */
|
||||
.codehilite .sh { color: #C3E88D } /* Literal.String.Heredoc */
|
||||
.codehilite .si { color: #89DDFF } /* Literal.String.Interpol */
|
||||
.codehilite .sx { color: #C3E88D } /* Literal.String.Other */
|
||||
.codehilite .sr { color: #89DDFF } /* Literal.String.Regex */
|
||||
.codehilite .s1 { color: #C3E88D } /* Literal.String.Single */
|
||||
.codehilite .ss { color: #89DDFF } /* Literal.String.Symbol */
|
||||
.codehilite .bp { color: #89DDFF } /* Name.Builtin.Pseudo */
|
||||
.codehilite .fm { color: #82AAFF } /* Name.Function.Magic */
|
||||
.codehilite .vc { color: #89DDFF } /* Name.Variable.Class */
|
||||
.codehilite .vg { color: #89DDFF } /* Name.Variable.Global */
|
||||
.codehilite .vi { color: #89DDFF } /* Name.Variable.Instance */
|
||||
.codehilite .vm { color: #82AAFF } /* Name.Variable.Magic */
|
||||
.codehilite .il { color: #F78C6C } /* Literal.Number.Integer.Long */
|
|
@ -1,46 +0,0 @@
|
|||
@media all and (max-width: 1399px) {
|
||||
body, input {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
header input[type="search"] {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#menu li {
|
||||
font-size: 10px;
|
||||
}
|
||||
#menu a {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
section {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1400px) {
|
||||
body, input {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
header input[type="search"] {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#menu li {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1199px) {
|
||||
.home-pinned-content article:nth-child(5) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 849px) {
|
||||
.home-pinned-content article:nth-child(4) {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -1,34 +1,34 @@
|
|||
#shoutbox {
|
||||
margin: 20px 5% 10px 5%;
|
||||
/*box-shadow: 0 0 2px rgba(0, 0, 0, .4);*/
|
||||
background: #ffffff;
|
||||
/*border: 1px solid #999999;*/
|
||||
margin: 20px 5% 10px 5%;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
#shoutbox > div {
|
||||
margin: 0; padding: 0; height: 125px; width: 100%;
|
||||
overflow-y: scroll; border-bottom: 1px solid #999999;
|
||||
border-radius: 5px 5px 0 0;
|
||||
border: 1px solid #999999;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 125px;
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
#shoutbox > input {
|
||||
width: 100%; padding: 5px 0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
border: 1px solid #999999;
|
||||
}
|
||||
#shoutbox > input:focus {
|
||||
border-color: #a12222;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(161, 34, 34, 0.6);
|
||||
}
|
||||
|
||||
#shoutbox > div > div {
|
||||
padding: 2px 10px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .3);
|
||||
font-size: 11px;
|
||||
}
|
||||
#shoutbox > div > div:last-child {
|
||||
border-bottom: none;
|
||||
padding: 2px 10px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
|
||||
font-size: 11px;
|
||||
}
|
||||
#shoutbox > div > div:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
background: var(--background);
|
||||
}
|
||||
#shoutbox > div > div:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
#shoutbox > input {
|
||||
width: 100%;
|
||||
padding: 5px 0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
#shoutbox > input:focus {
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(161, 34, 34, 0.6);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/* SimpleMDE overwrite that allows us to customize from themes */
|
||||
|
||||
div.editor-toolbar {
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
div.editor-toolbar > a {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
div.editor-toolbar > a.active,
|
||||
div.editor-toolbar > a:hover {
|
||||
background: var(--background-light);
|
||||
border-color: var(--background-light);
|
||||
}
|
||||
|
||||
div.editor-toolbar > i.separator {
|
||||
border-right-color: transparent;
|
||||
border-left-color: var(--separator);
|
||||
}
|
||||
|
||||
div.editor-toolbar.disabled-for-preview a:not(.no-disable) {
|
||||
background: none;
|
||||
color: var(--text-disabled) !important;
|
||||
}
|
||||
div.editor-toolbar.disabled-for-preview > i.separator {
|
||||
border-left-color: var(--text-disabled);
|
||||
}
|
||||
|
||||
div.CodeMirror,
|
||||
div.editor-preview {
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
div.editor-preview {
|
||||
background: var(--background-preview);
|
||||
}
|
||||
|
||||
div.editor-preview table th,
|
||||
div.editor-preview-side table th,
|
||||
div.editor-preview table td,
|
||||
div.editor-preview-side table td {
|
||||
border: inherit;
|
||||
padding: inherit;
|
||||
}
|
||||
|
||||
div.editor-preview table.codehilitetable pre,
|
||||
div.editor-preview-side table.codehilitetable pre {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
div.CodeMirror .CodeMirror-selected,
|
||||
div.CodeMirror .CodeMirror-selectedtext {
|
||||
background: var(--background-light);
|
||||
}
|
||||
div.CodeMirror .CodeMirror-focused .CodeMirror-selected,
|
||||
div.CodeMirror .CodeMirror-focused .CodeMirror-selectedtext,
|
||||
div.CodeMirror .CodeMirror-line::selection,
|
||||
div.CodeMirror .CodeMirror-line > span::selection,
|
||||
div.CodeMirror .CodeMirror-line > span > span::selection {
|
||||
background: var(--background-light);
|
||||
}
|
||||
div.CodeMirror .CodeMirror-line::-moz-selection,
|
||||
div.CodeMirror .CodeMirror-line > span::-moz-selection,
|
||||
div.CodeMirror .CodeMirror-line > span > span::-moz-selection {
|
||||
background: var(--background-light);
|
||||
}
|
||||
|
||||
div.CodeMirror-cursor {
|
||||
border-color: var(--text);
|
||||
}
|
|
@ -3,6 +3,11 @@ table {
|
|||
border-color: var(--border);
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px 0;
|
||||
/* Code fragments */
|
||||
/* Forum and sub-forum listings */
|
||||
/* Topic table */
|
||||
/* Thread table */
|
||||
/* Tables with filters */
|
||||
}
|
||||
table tr:nth-child(even) {
|
||||
background: var(--background);
|
||||
|
@ -17,37 +22,58 @@ table th {
|
|||
border-width: 1px 0;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
table td {
|
||||
padding: 4px 6px;
|
||||
table:not(.codehilitetable) td {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
table.codehilitetable {
|
||||
border: none;
|
||||
width: 100%;
|
||||
/* This seems to be the only way to prevent the automatic table size
|
||||
algorithm from assigning extreme widths when content overflows.
|
||||
Fortunately the structure of the table is very simple, so we can afford to
|
||||
set a fixed width on line numbers and move on */
|
||||
table-layout: fixed;
|
||||
}
|
||||
table.codehilitetable tr:nth-child(even),
|
||||
table.codehilitetable tr:nth-child(odd) {
|
||||
background: none;
|
||||
}
|
||||
table.codehilitetable td {
|
||||
padding: 0;
|
||||
}
|
||||
table.codehilitetable td.linenos {
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
table.codehilitetable td.linenos span {
|
||||
padding-right: 8px;
|
||||
}
|
||||
table.codehilitetable td.code,
|
||||
table.codehilitetable pre {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
table.codehilitetable pre {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Forum and sub-forum listings */
|
||||
|
||||
table.forumlist {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
|
||||
margin: 16px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* table.forumlist th {
|
||||
background: #d05950;
|
||||
border-color: #b04940;
|
||||
color: white;
|
||||
} */
|
||||
|
||||
table.forumlist tr {
|
||||
background: unset;
|
||||
}
|
||||
table.forumlist tr > td:last-child,
|
||||
table.forumlist tr > th:last-child {
|
||||
width: 20%;
|
||||
text-align: center;
|
||||
}
|
||||
table.forumlist tr:nth-child(4n+2),
|
||||
table.forumlist tr:nth-child(4n+3) {
|
||||
background: rgba(0, 0, 0, .05);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
|
||||
/* Topic table */
|
||||
|
||||
table.topiclist {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
|
@ -56,18 +82,11 @@ table.topiclist tr > *:nth-child(n+2) {
|
|||
/* This matches all children except the first column */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
table.forumlist th > td:last-child,
|
||||
table.forumlist tr > td:last-child,
|
||||
table.topiclist th > td:last-child,
|
||||
table.topiclist tr > td:last-child {
|
||||
width: 20%; text-align: center;
|
||||
table.topiclist tr > td:last-child,
|
||||
table.topiclist tr > th:last-child {
|
||||
width: 20%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
/* Thread table */
|
||||
|
||||
table.thread {
|
||||
width: 100%;
|
||||
border-width: 1px 0;
|
||||
|
@ -75,39 +94,88 @@ table.thread {
|
|||
table.thread.topcomment {
|
||||
border: none;
|
||||
}
|
||||
table.thread td.author {
|
||||
width: 256px;
|
||||
table.thread.topcomment td.message {
|
||||
padding-top: 0;
|
||||
}
|
||||
table.thread.topcomment div.info {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
table.thread td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table.thread div.info {
|
||||
float: right;
|
||||
text-align: right;
|
||||
opacity: 0.7;
|
||||
table.thread td.author {
|
||||
width: 256px;
|
||||
}
|
||||
@media screen and (max-width: 1199px) {
|
||||
table.thread td.author {
|
||||
/* Includes padding */
|
||||
width: 136px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 849px) {
|
||||
table.thread td.author {
|
||||
width: 104px;
|
||||
}
|
||||
}
|
||||
table.thread td.message {
|
||||
padding-top: 8px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
table.thread td.message > *:nth-child(2) {
|
||||
margin-top: 0;
|
||||
}
|
||||
table.thread td.message img {
|
||||
max-width: 100%;
|
||||
}
|
||||
table.thread:not(.topcomment) div.info {
|
||||
float: right;
|
||||
}
|
||||
table.thread div.info {
|
||||
text-align: right;
|
||||
position: relative;
|
||||
}
|
||||
@media screen and (max-width: 1199px) {
|
||||
table.thread div.info {
|
||||
float: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: 0;
|
||||
}
|
||||
table.thread div.info > *:not(:last-child):after {
|
||||
content: '·';
|
||||
margin: 0 4px;
|
||||
}
|
||||
table.thread td.author {
|
||||
/* Includes padding */
|
||||
width: 136px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tables with filters */
|
||||
|
||||
table.thread div.info > * {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.thread div.info summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
table.thread .topcomment-placeholder div {
|
||||
font-style: italic;
|
||||
opacity: 0.5;
|
||||
padding: 8px 0;
|
||||
}
|
||||
table.thread .context-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
box-shadow: var(--shadow);
|
||||
border-radius: 4px;
|
||||
transition: none;
|
||||
background: var(--background);
|
||||
z-index: 2;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
table.thread .context-menu a {
|
||||
display: block;
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
color: inherit;
|
||||
}
|
||||
table.thread .context-menu a:hover {
|
||||
background: var(--background-light);
|
||||
text-decoration: none;
|
||||
}
|
||||
table.filter-target th:after {
|
||||
content: attr(data-filter);
|
||||
display: block;
|
||||
|
|
|
@ -10,10 +10,11 @@
|
|||
*/
|
||||
|
||||
:root {
|
||||
--background: #1c2124; /*22292c, 1c2124, 1E1E1E, 242424,*/
|
||||
--text: #f2f2f2;
|
||||
--background: #171a1c; /*22292c, 1c2124, 1E1E1E, 242424,*/
|
||||
--text: #eaeaea;
|
||||
--text-light: #e2e2e2;
|
||||
|
||||
--links: #fe2d2d;
|
||||
--links: #db3f3f;
|
||||
|
||||
--ok: #149641;
|
||||
--ok-text: #ffffff;
|
||||
|
@ -31,14 +32,13 @@
|
|||
--info-text: #ffffff;
|
||||
--info-active: #215ab0;
|
||||
|
||||
--hr-border: 1px solid #b0b0b0;
|
||||
--hr-border: 1px solid #404040;
|
||||
}
|
||||
|
||||
.form {
|
||||
--background: #ffffff;
|
||||
--text: #000000;
|
||||
--border: 1px solid #c8c8c8;
|
||||
--border-focused: #7cade0;
|
||||
--background: #1c2124;
|
||||
--border: 1px solid #303030;
|
||||
--border-focused: #577799;
|
||||
--shadow-focused: rgba(87, 143, 228, 0.5);
|
||||
}
|
||||
|
||||
|
@ -74,13 +74,13 @@
|
|||
|
||||
|
||||
header {
|
||||
--background: #0d1215; /*5a5a5a*/
|
||||
--background: #0d1215;
|
||||
--text: #000000;
|
||||
--border: 1px solid #d0d0d0;
|
||||
--border: 1px solid #404040;
|
||||
}
|
||||
|
||||
footer {
|
||||
--background: rgba(0, 0, 0, 1); /* #ffffff */
|
||||
--background: #0d1215;
|
||||
--text: #a0a0a0;
|
||||
--border: #d0d0d0;
|
||||
}
|
||||
|
@ -102,12 +102,36 @@ footer {
|
|||
}
|
||||
|
||||
.profile-xp {
|
||||
--background: #e0e0e0;
|
||||
--border: 1px solid #c0c0c0;
|
||||
--background: #4a4a4a;
|
||||
--border: 1px solid #5a5a5a;
|
||||
--background-xp: #f85555;
|
||||
--border-xp: 1px solid #d03333;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
--background: #1d2326;
|
||||
--shadow: 0 0 12px -9px #000000;
|
||||
--border: #404040;
|
||||
--background-light: #262c2f;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) { --background: rgba(255, 255, 255, 0.15); }
|
||||
table tr:nth-child(odd) { --background: #1c2124; } /* 22292c = background, 1c2124, 1e1e1e*/
|
||||
table {
|
||||
--border: #404040;
|
||||
}
|
||||
table tr:nth-child(even) {
|
||||
--background: #262c2f;
|
||||
}
|
||||
table tr:nth-child(odd) {
|
||||
--background: #1d2326;
|
||||
}
|
||||
table.codehilitetable {
|
||||
--background: #263238;
|
||||
}
|
||||
|
||||
div.editor-toolbar, div.CodeMirror {
|
||||
--border: #404040;
|
||||
--background-light: #404040;
|
||||
--background-preview: #1c2124;
|
||||
--separator: #404040;
|
||||
--text-disabled: #262c2f;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/* Theme metadata
|
||||
@NAME: Tituya's V43 theme
|
||||
@AUTHOR: Tituya
|
||||
*/
|
||||
|
||||
:root {
|
||||
--background: #fff;
|
||||
--text: #030303;
|
||||
--text-light: #b62727;
|
||||
|
||||
--links: #c02020;
|
||||
|
||||
--ok: #b62727;
|
||||
--ok-text: #ffffff;
|
||||
--ok-active: #950c0c;
|
||||
|
||||
--warn: #ffa01a;
|
||||
--warn-text: #000;
|
||||
--warn-active: #ff9600;
|
||||
|
||||
--error: #d23a2f;
|
||||
--error-text: #ffffff;
|
||||
--error-active: #b32a20;
|
||||
|
||||
--info: #2e7aec;
|
||||
--info-text: #ffffff;
|
||||
--info-active: #215ab0;
|
||||
|
||||
--hr-border: 1px solid #aaa2a2;
|
||||
}
|
||||
|
||||
.form {
|
||||
--background: #fff;
|
||||
--text: #000;
|
||||
--border: 1px solid #aaa2a2;
|
||||
--border-focused: #577799;
|
||||
--shadow-focused: rgba(87, 143, 228, 0.5);
|
||||
}
|
||||
|
||||
.editor button {
|
||||
--background: #ffffff;
|
||||
--text: #000000;
|
||||
--border: 1px solid rgba(0, 0, 0, 0);
|
||||
--border-focused: 1px solid rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
#light-menu {
|
||||
--background: linear-gradient(90deg, rgba(192,26,26,1) 0%, rgb(119, 16, 16) 150%);
|
||||
--text: #ffffff;
|
||||
--icons: #ffffff;
|
||||
--shadow: 0 0 10px #a49594;
|
||||
|
||||
--logo-bg: transparent;
|
||||
--logo-shadow: 0 0 2px rgba(0, 0, 0, .7);
|
||||
--logo-active: linear-gradient(90deg, rgb(165, 22, 22) 0%, rgb(117, 13, 13) 95%);
|
||||
}
|
||||
|
||||
#menu {
|
||||
--background: #fff;
|
||||
--text: #030303;
|
||||
--shadow: 0 0 8px rgb(0, 0, 0);
|
||||
|
||||
--input-bg: #fff;
|
||||
--input-text: #000;
|
||||
--input-border: 1px solid #aaa2a2;
|
||||
}
|
||||
|
||||
#menu.opened svg > path {
|
||||
fill: #be1818;
|
||||
}
|
||||
|
||||
header {
|
||||
--background: #fff;
|
||||
--border: 1px solid #be1818;
|
||||
}
|
||||
|
||||
footer {
|
||||
--background: #fff;
|
||||
--text: #a0a0a0;
|
||||
}
|
||||
|
||||
.flash {
|
||||
--background: #ffffff;
|
||||
--text: #000;
|
||||
--shadow: 0 1px 12px rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* Uncomment to inherit :root values
|
||||
--ok: #149641;
|
||||
--warn: #f59f25;
|
||||
--error: #d23a2f;
|
||||
--info: #2e7aec; */
|
||||
|
||||
--btn-bg: rgba(0, 0, 0, 0);
|
||||
--btn-text: #000000;
|
||||
--btn-bg-active: rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.profile-xp {
|
||||
--background: #fff;
|
||||
--border: 1px solid #be1818;
|
||||
--background-xp: #be1818;
|
||||
--border-xp: 1px solid #be1818;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
--background: #ffffff;
|
||||
--shadow: 0 0 12px -9px #000000;
|
||||
--border: #d0d0d0;
|
||||
--background-light: #f0f0f0;
|
||||
}
|
||||
|
||||
div.pagination {
|
||||
font-size: 14px;
|
||||
margin: 13px;
|
||||
}
|
||||
|
||||
table {
|
||||
--border: #000;
|
||||
}
|
||||
table tr:nth-child(even) {
|
||||
--background: #fff;
|
||||
}
|
||||
|
||||
table tr:nth-child(odd) {
|
||||
--background: #ecb0b0;
|
||||
}
|
||||
|
||||
table.codehilitetable tr {
|
||||
--background: #f0f0f0;
|
||||
}
|
||||
|
||||
div.editor-toolbar {
|
||||
--border: #aaa2a2;
|
||||
--background-light: #c0c0c0;
|
||||
--background-preview: #fff;
|
||||
--separator: #aaa2a2;
|
||||
--text-disabled: #c0c0c0;
|
||||
--text: #515151;
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
:root {
|
||||
--background: #ffffff;
|
||||
--text: #000000;
|
||||
--text-light: #101010;
|
||||
|
||||
--links: #c61a1a;
|
||||
|
||||
|
@ -22,18 +23,18 @@
|
|||
--info-text: #ffffff;
|
||||
--info-active: #215ab0;
|
||||
|
||||
--hr-border: 1px solid #b0b0b0;
|
||||
--hr-border: 1px solid #d8d8d8;
|
||||
}
|
||||
|
||||
table {
|
||||
--border: #d8d8d8;
|
||||
}
|
||||
table tr:nth-child(even) {
|
||||
table tr:nth-child(odd) {
|
||||
--background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
table th {
|
||||
--background: #e0e0e0;
|
||||
--border: #d0d0d0;
|
||||
--background: #e0e0e0;
|
||||
--border: #d0d0d0;
|
||||
}
|
||||
|
||||
.form {
|
||||
|
@ -76,7 +77,7 @@ table th {
|
|||
header {
|
||||
--background: #f4f4f6;
|
||||
--text: #000000;
|
||||
--border: 1px solid #d0d0d0;
|
||||
--border: 1px solid #d8d8d8;
|
||||
}
|
||||
|
||||
footer {
|
||||
|
@ -108,3 +109,23 @@ footer {
|
|||
--background-xp-100: #d03333;
|
||||
--border-xp: 1px solid #d03333;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
--background: #ffffff;
|
||||
--shadow: 0 0 12px -9px #000000;
|
||||
--border: #d0d0d0;
|
||||
--background-light: #f0f0f0;
|
||||
}
|
||||
|
||||
div.editor-toolbar, div.CodeMirror {
|
||||
--border: #c0c0c0;
|
||||
--background-light: #d9d9d9;
|
||||
--background-preview: #f4f4f6;
|
||||
--separator: #a0a0a0;
|
||||
--text-disabled: #c0c0c0;
|
||||
}
|
||||
|
||||
/* Extra style on top of the Pygments style */
|
||||
table.codehilitetable td.linenos {
|
||||
color: #808080;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/* Currently 3 screen sizes supported :
|
||||
- micro: < 500px
|
||||
- tiny: < 850px
|
||||
- small: < 1200px
|
||||
- normal: >= 1200px
|
||||
|
||||
Ex:
|
||||
@media screen and (max-width: @var)
|
||||
*/
|
|
@ -1,139 +1,140 @@
|
|||
/* Currently 3 screen sizes supported :
|
||||
- tiny: < 850px
|
||||
- small: < 1200px
|
||||
- normal: >= 1200px
|
||||
|
||||
Ex:
|
||||
@media screen and (max-width: @var)
|
||||
*/
|
||||
/* Profile summaries */
|
||||
|
||||
.profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 265px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 265px;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin-right: 16px;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.profile-title {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.profile-points {
|
||||
font-size: 11px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.profile-points span {
|
||||
color: gray;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.profile-points-small {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-xp {
|
||||
height: 10px;
|
||||
min-width: 96px;
|
||||
background: var(--background);
|
||||
border: var(--border);
|
||||
height: 10px;
|
||||
min-width: 96px;
|
||||
max-width: 96px;
|
||||
background: var(--background);
|
||||
border: var(--border);
|
||||
}
|
||||
|
||||
.profile-xp-100 {
|
||||
background: var(--background-xp);
|
||||
border: var(--border-xp);
|
||||
background: var(--background-xp);
|
||||
border: var(--border-xp);
|
||||
}
|
||||
|
||||
.profile-xp div {
|
||||
height: 10px;
|
||||
background: var(--background-xp);
|
||||
border: var(--border-xp);
|
||||
margin: -1px;
|
||||
height: 10px;
|
||||
background: var(--background-xp);
|
||||
border: var(--border-xp);
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
.profile-xp-100 div {
|
||||
background: var(--background-xp-100);
|
||||
background: var(--background-xp-100);
|
||||
}
|
||||
|
||||
.profile.guest {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile.guest em {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1199px) {
|
||||
table.thread .profile {
|
||||
flex-direction: column;
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
table.thread .profile-avatar {
|
||||
order: 1;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
table.thread .profile-title,
|
||||
table.thread .profile-points,
|
||||
table.thread .profile-xp {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table.thread .profile-points-small {
|
||||
display: inline;
|
||||
}
|
||||
@media (max-width: 1199px) {
|
||||
table.thread .profile {
|
||||
flex-direction: column;
|
||||
width: 96px;
|
||||
text-align: center;
|
||||
}
|
||||
table.thread .profile-avatar {
|
||||
order: 1;
|
||||
margin-right: 0;
|
||||
margin-top: 4px;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
table.thread .profile-title,
|
||||
table.thread .profile-points,
|
||||
table.thread .profile-xp {
|
||||
display: none;
|
||||
}
|
||||
table.thread .profile-points-small {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
@media (max-width: 849px) {
|
||||
table.thread .profile {
|
||||
width: 96px;
|
||||
}
|
||||
table.thread .profile-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Trophies */
|
||||
.trophies {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.trophy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 260px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
border: 1px solid #c5c5c5;
|
||||
border-left: 5px solid var(--links);
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 260px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
border: 1px solid #c5c5c5;
|
||||
border-left: 5px solid var(--links);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.trophy img {
|
||||
height: 48px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.trophy div > * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.trophy em {
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.trophy span {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.trophy.disabled {
|
||||
filter: grayscale(100%);
|
||||
opacity: .5;
|
||||
border-left: 1px solid #c5c5c5;
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.5;
|
||||
border-left: 1px solid #c5c5c5;
|
||||
}
|
||||
.trophy.form-disabled {
|
||||
border-left: 1px solid #c5c5c5;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.trophy img {
|
||||
height: 48px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.trophy div > * {
|
||||
display: block;
|
||||
}
|
||||
.trophy em {
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.trophy span {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
hr.signature {
|
||||
opacity: 0.2;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
@import "vars";
|
||||
|
||||
.container {
|
||||
margin-left: 110px;
|
||||
|
||||
@media screen and (max-width: @tiny) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
width: 80%;
|
||||
margin: 20px auto 0 auto;
|
||||
|
||||
@media screen and (max-width: @normal) {
|
||||
width: 90%;
|
||||
}
|
||||
@media screen and (max-width: @small) {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
flash overlay
|
||||
*/
|
||||
|
||||
.flash {
|
||||
margin: 5px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 80%;
|
||||
font-size: 14px;
|
||||
border-bottom: 5px solid var(--info);
|
||||
border-radius: 1px;
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
&.info {
|
||||
border-color: var(--info);
|
||||
}
|
||||
|
||||
&.ok {
|
||||
border-color: var(--ok);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-color: var(--warn);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
margin: 15px 10px 10px 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin: 15px 20px 10px 30px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
Footer
|
||||
*/
|
||||
|
||||
footer {
|
||||
margin: 20px 0 0 0; padding: 10px 10%;
|
||||
text-align: center; font-size: 11px; font-style: italic;
|
||||
background: var(--background); color: var(--text);
|
||||
border-top: var(--border);
|
||||
|
||||
p {
|
||||
margin: 3px 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/* Full-page forms */
|
||||
|
||||
.form {
|
||||
form {
|
||||
& > div:not(:last-child):not(.editor-toolbar) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin: 0 5px 4px 0;
|
||||
|
||||
& + .desc {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 80%;
|
||||
opacity: .75;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 128px; height: 128px;
|
||||
|
||||
& + input[type="file"] {
|
||||
margin: 16px 0 0 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
input[type='email'],
|
||||
input[type='date'],
|
||||
input[type='password'],
|
||||
input[type='search'],
|
||||
textarea,
|
||||
select {
|
||||
display: block;
|
||||
width: 100%; padding: 6px 8px;
|
||||
background: var(--background); color: var(--text);
|
||||
border: var(--border);
|
||||
|
||||
/* Transitions when resizing with the mouse produces apparent lag */
|
||||
transition: all .15s ease, width 0s, height 0s;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--border-focused);
|
||||
box-shadow: 0 0 0 3px var(--shadow-focused);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
/* Override an annoying Firefox default */
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
margin: 0 4px 0 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
max-width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
progress.entropy {
|
||||
display: none; /* Display with Js enabled */
|
||||
width: 100%; margin-top: 5px;
|
||||
background: var(--background);
|
||||
border: var(--border);
|
||||
|
||||
&.low {
|
||||
&::-moz-progress-bar,
|
||||
&::-webkit-progress-bar {
|
||||
background: var(--error);
|
||||
}
|
||||
}
|
||||
&.medium {
|
||||
&::-moz-progress-bar,
|
||||
&::-webkit-progress-bar {
|
||||
background: var(--warn);
|
||||
}
|
||||
}
|
||||
&.high {
|
||||
&::-moz-progress-bar,
|
||||
&::-webkit-progress-bar {
|
||||
background: var(--ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 3px;
|
||||
border: var(--hr-border);
|
||||
border-width: 1px 0;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.msgerror {
|
||||
color: var(--error);
|
||||
font-weight: 400;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* anti-bots field */
|
||||
.abfield {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Interactive filter forms */
|
||||
|
||||
.form.filter {
|
||||
margin-bottom: 16px;
|
||||
|
||||
& > p:first-child {
|
||||
font-size: 80%;
|
||||
color: gray;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.syntax-explanation {
|
||||
font-size: 80%;
|
||||
color: gray;
|
||||
margin-top: 8px;
|
||||
|
||||
ul {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
padding-left: 16px;
|
||||
line-height: 20px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
code {
|
||||
background: rgba(0,0,0,.05);
|
||||
padding: 1px 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
@import "vars";
|
||||
|
||||
/* Fonts */
|
||||
|
||||
@font-face { font-family: NotoSans; src: url(../fonts/noto_sans.ttf); font-display: swap; }
|
||||
@font-face { font-family: Twemoji; src: url(../fonts/TwitterColorEmoji.ttf); font-display: swap; }
|
||||
@font-face { font-family: Cantarell; font-weight: normal; src: url(../fonts/Cantarell-Regular.otf); font-display: swap; }
|
||||
@font-face { font-family: Cantarell; font-weight: bold; src: url(../fonts/Cantarell-Bold.otf); font-display: swap; }
|
||||
|
||||
/* Whole page */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
/* This transition value is replicated everywhere transitions are customized,
|
||||
make sure to track them when editing */
|
||||
transition: .15s ease;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
font-family: 'DejaVu Sans', sans-serif;
|
||||
font-size: 13px;
|
||||
|
||||
@media screen and (min-width: @normal) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* General */
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--links);
|
||||
|
||||
&:hover, &:focus {
|
||||
text-decoration: underline;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
p {
|
||||
line-height: 20px;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
|
||||
ul {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
border-bottom: var(--hr-border);
|
||||
font-family: Cantarell; font-weight: bold;
|
||||
font-size: 26px;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 24px 0 16px 0;
|
||||
border-bottom: var(--hr-border);
|
||||
font-family: Cantarell; font-weight: bold;
|
||||
font-size: 18px;
|
||||
color: var(--text-light);
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.button, input[type="button"], input[type="submit"] {
|
||||
padding: 6px 10px; border-radius: 2px;
|
||||
cursor: pointer;
|
||||
font-family: 'DejaVu Sans', sans-serif; font-weight: 400;
|
||||
border: 0;
|
||||
|
||||
&:hover, &:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Bootstrap-style rules */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bg-ok {
|
||||
background: var(--ok);
|
||||
color: var(--ok-text);
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
background: var(--ok-active);
|
||||
}
|
||||
}
|
||||
|
||||
.bg-error {
|
||||
background: var(--error);
|
||||
color: var(--error-text);
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
background: var(--error-active);
|
||||
}
|
||||
}
|
||||
|
||||
.bg-warn {
|
||||
background: var(--warn);
|
||||
color: var(--warn-text);
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
background: var(--warn-active);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.skip-to-content-link {
|
||||
height: 30px;
|
||||
left: 50%;
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
transform: translateY(-100%);
|
||||
transition: transform 0.3s;
|
||||
background: var(--links);
|
||||
color: var(--warn-text);
|
||||
border-radius: 1px;
|
||||
|
||||
&:focus {
|
||||
transform: translateY(0%);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
@import "vars";
|
||||
|
||||
header {
|
||||
margin: 0; padding: 8px 16px;
|
||||
background: var(--background); border-bottom: var(--border);
|
||||
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
flex-flow: row wrap;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
margin: 4px 0;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: Cantarell; font-weight: bold; font-size: 18px;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-right: -26px;
|
||||
|
||||
@media screen and (max-width: @tiny) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="search"] {
|
||||
display: inline-block; width: 250px;
|
||||
padding: 5px 35px 5px 10px;
|
||||
|
||||
@media screen and (max-width: @small) {
|
||||
width: 200px;
|
||||
}
|
||||
@media screen and (min-width: @normal) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
& ~ a {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
& ~ a {
|
||||
position: relative; left: -33px;
|
||||
opacity: .7;
|
||||
|
||||
& > svg > path {
|
||||
fill: var(--text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
fill: #363636;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &:focus {
|
||||
& > svg {
|
||||
fill: var(--text);
|
||||
}
|
||||
}
|
||||
}
|
||||
svg {
|
||||
width: 24px; height: 24px; vertical-align: middle;
|
||||
transition: .15s ease;
|
||||
}
|
||||
}
|
||||
|
||||
#spotlight {
|
||||
margin-left: 16px;
|
||||
|
||||
@media screen and (max-width: @small) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#server-speed-warning {
|
||||
background: var(--warn);
|
||||
color: var(--warn-text);
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
padding: 4px;
|
||||
margin: 0 8px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 1px 1px rgba(0,0,0,.5);
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
home-title
|
||||
*/
|
||||
|
||||
.home-title {
|
||||
margin: 20px 0; padding: 10px 5%;
|
||||
background: #bf1c11; box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
|
||||
border-top: 10px solid #ab170c;
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #ffffff; border-color: #ffffff;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0; text-align: justify;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit; text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
pinned-content
|
||||
*/
|
||||
|
||||
.home-pinned-content {
|
||||
& > div {
|
||||
display: flex; justify-content: space-between;
|
||||
}
|
||||
|
||||
h2 {
|
||||
display: block; margin: 5px 0;
|
||||
font-size: 18px; font-family: NotoSans; font-weight: 200;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
|
||||
&:hover, &:focus {
|
||||
img {
|
||||
filter: blur(3px);
|
||||
}
|
||||
div {
|
||||
padding: 200px 5% 10px 5%;
|
||||
background-image: linear-gradient(180deg,transparent 0,rgba(0,0,0,.7) 40px,rgba(0,0,0,.8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%; filter: blur(0px);
|
||||
}
|
||||
|
||||
article {
|
||||
flex-grow: 1; margin: 0 1px; padding: 0;
|
||||
position: relative;
|
||||
max-width: 250px; overflow: hidden;
|
||||
|
||||
div {
|
||||
position: absolute; bottom: 0; z-index: 3;
|
||||
width: 90%; margin: 0;
|
||||
padding: 30px 5% 10px 5%;
|
||||
color: #ffffff; text-shadow: 1px 1px 0 rgba(0,0,0,.6);
|
||||
background-image: linear-gradient(180deg,transparent 0,rgba(0,0,0,.7) 40px,rgba(0,0,0,.8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
home-articles
|
||||
*/
|
||||
|
||||
.home-articles {
|
||||
display: flex; justify-content: space-between;
|
||||
|
||||
& > div {
|
||||
flex-grow: 1; max-width: 48%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
|
||||
a {
|
||||
padding: 0;
|
||||
font-family: NotoSans; font-size: 16px;
|
||||
font-weight: 400; color: /*#015078*/ /*#bf1c11*/ #234d5f;
|
||||
|
||||
&:hover, &:focus {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 5px 0;
|
||||
text-align: justify;
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 10px; margin: 10px 0; display: flex; align-items: center;
|
||||
background: #ffffff; border: 1px solid rgba(0, 0, 0, .2);
|
||||
|
||||
& > img {
|
||||
float: left; margin-right: 10px; flex-shrink: 0;
|
||||
|
||||
&.screeshot {
|
||||
width: 128px; height: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
& > div {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #424242; font-weight: normal;
|
||||
}
|
||||
|
||||
a:hover, a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.metadata {
|
||||
margin: 0;
|
||||
color: #22292c;
|
||||
|
||||
a {
|
||||
color: #22292c; font-weight: 400; font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
@import "vars";
|
||||
|
||||
nav a {
|
||||
opacity: .8;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Menu */
|
||||
|
||||
#logo {
|
||||
position: relative; display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
opacity: 1;
|
||||
background: var(--logo-bg);
|
||||
transition: .15s ease;
|
||||
|
||||
@media screen and (max-width: @tiny) {
|
||||
width: auto; height: 100%; margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background: var(--logo-active);
|
||||
|
||||
img {
|
||||
filter: drop-shadow(var(--logo-shadow));
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
display: block; height: 65px;
|
||||
margin: 0 auto; padding: 0;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, .0));
|
||||
transition: filter .15s ease;
|
||||
|
||||
@media screen and (max-width: @small) {
|
||||
width: 60px; height: inherit;
|
||||
margin-bottom: -4.5px;
|
||||
}
|
||||
@media screen and (max-width: @micro) {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#light-menu {
|
||||
position: fixed; z-index: 10;
|
||||
list-style: none;
|
||||
width: 110px;
|
||||
height: 100%; overflow-y: auto;
|
||||
margin: 0; padding: 0;
|
||||
text-indent: 0; font-size: 13px;
|
||||
background: var(--background); box-shadow: var(--shadow);
|
||||
|
||||
@media screen and (max-width: @tiny) {
|
||||
position: unset;
|
||||
display: flex; flex-direction: row; align-items: center;
|
||||
width: 100%; height: 60px;
|
||||
overflow-x: auto; overflow-y: hidden;
|
||||
}
|
||||
@media screen and (max-width: @micro) {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
color: var(--text);
|
||||
|
||||
@media screen and (max-width: @tiny) {
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; flex-grow: 1;
|
||||
padding: 0 2px; font-size: 12px;
|
||||
}
|
||||
|
||||
& > a {
|
||||
display: flex; flex-direction: column; flex-grow: 0;
|
||||
align-items: center; justify-content: center;
|
||||
width: 100%; height: 100%;
|
||||
margin: 20px 0;
|
||||
color: var(--text);
|
||||
transition: opacity .15s ease; /* because Chrome sucks */
|
||||
|
||||
/* Avatar */
|
||||
& > img {
|
||||
display: block; width: 60px; flex-shrink: 0; flex-grow: 0;
|
||||
margin: 0 7px 5px 7px;
|
||||
border-radius: 10%;
|
||||
|
||||
@media screen and (max-width: @tiny) {
|
||||
width: 45px;
|
||||
margin: 0;
|
||||
|
||||
& ~ div {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: @micro) {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
& > svg {
|
||||
display: block; width: 25px; flex-shrink: 0; flex-grow: 0;
|
||||
margin: 0 7px;
|
||||
|
||||
& > path {
|
||||
fill: var(--icons);
|
||||
}
|
||||
}
|
||||
|
||||
& > div {
|
||||
@media screen and (max-width: @micro) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Overlay */
|
||||
#menu {
|
||||
position: fixed; z-index: 5;
|
||||
left: -190px; width: 300px; /* default: left-to-right animation */
|
||||
height: 100%; overflow-x: hidden; overflow-y: auto;
|
||||
font-size: 13px;
|
||||
background: var(--background); color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
transition: .15s ease;
|
||||
|
||||
@media screen and (max-width: @tiny) {
|
||||
width: 100%; height: 0; overflow-x: hidden;
|
||||
font-family: NotoSans;
|
||||
transition: .1s ease;
|
||||
position: unset;
|
||||
left: unset;
|
||||
}
|
||||
|
||||
&.opened {
|
||||
left: 110px;
|
||||
|
||||
@media screen and (max-width: @tiny) {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
left: unset;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set class="scroll-animation" to menu to apply scroll animation */
|
||||
&.scroll-animation {
|
||||
left: 110px; width: 0;
|
||||
&.opened {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
& > div {
|
||||
width: 300px;
|
||||
padding: 16px;
|
||||
display: none;
|
||||
|
||||
@media screen and (max-width: @tiny) {
|
||||
width: 100%;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
&.opened {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 20px 0;
|
||||
font-family: Cantarell; font-weight: bold; font-size: 18px;
|
||||
color: var(--text);
|
||||
display: flex; align-items: center;
|
||||
|
||||
@media screen and (max-width: @micro) {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
a {
|
||||
margin: 0;
|
||||
font-size: inherit; opacity: inherit;
|
||||
|
||||
&:hover, &:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
& > svg {
|
||||
width: 32px; vertical-align: middle; margin-right: 8px;
|
||||
|
||||
@media screen and (max-width: @micro) {
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
height: 48px; vertical-align: middle; margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 16px 0;
|
||||
font-family: Cantarell; font-weight: bold; font-size: 15px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 15px 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0; padding: 0; list-style: none;
|
||||
}
|
||||
|
||||
a, li {
|
||||
display: block; margin: 10px 0;
|
||||
color: var(--text);
|
||||
transition: opacity .15s ease;
|
||||
}
|
||||
|
||||
li > a {
|
||||
display: inline;
|
||||
margin: 0; font-style: normal;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
a > img {
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
a > svg {
|
||||
width: 20px; height: 20px; vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Login form */
|
||||
form {
|
||||
padding: 0 8%;
|
||||
|
||||
@media screen and (max-width: @tiny) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
margin: 8px 0; padding: 5px 2%;
|
||||
font-size: 14px;
|
||||
border: var(--input-border);
|
||||
background: var(--input-bg);
|
||||
color: var(--input-text);
|
||||
opacity: .8;
|
||||
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
width: 100%;
|
||||
margin: 8px 0 5px 0;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 13px; opacity: .8;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.pagination {
|
||||
text-align: center;
|
||||
margin: 5px 0;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
#shoutbox {
|
||||
margin: 20px 5% 10px 5%;
|
||||
background: #ffffff;
|
||||
|
||||
& > div {
|
||||
margin: 0; padding: 0; height: 125px; width: 100%;
|
||||
overflow-y: scroll; border-bottom: 1px solid var(--border);
|
||||
border-radius: 5px 5px 0 0;
|
||||
|
||||
& > div {
|
||||
padding: 2px 10px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .3);
|
||||
font-size: 11px;
|
||||
|
||||
&:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > input {
|
||||
width: 100%; padding: 5px 0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
border: 1px solid var(--border);
|
||||
|
||||
&:focus {
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(161, 34, 34, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
@import "vars";
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-color: var(--border);
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px 0;
|
||||
|
||||
tr {
|
||||
&:nth-child(even) {
|
||||
background: var(--background);
|
||||
}
|
||||
&:nth-child(odd) {
|
||||
background: var(--background);
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--background);
|
||||
border-color: var(--border);
|
||||
border-style: solid;
|
||||
border-width: 1px 0;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
&:not(.codehilitetable) td {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
/* Code fragments */
|
||||
&.codehilitetable {
|
||||
border: none;
|
||||
width: 100%;
|
||||
/* This seems to be the only way to prevent the automatic table size
|
||||
algorithm from assigning extreme widths when content overflows.
|
||||
Fortunately the structure of the table is very simple, so we can afford to
|
||||
set a fixed width on line numbers and move on */
|
||||
table-layout: fixed;
|
||||
|
||||
tr:nth-child(even),
|
||||
tr:nth-child(odd) {
|
||||
background: none;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0;
|
||||
|
||||
&.linenos {
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
span {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td.code,
|
||||
pre {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Forum and sub-forum listings */
|
||||
&.forumlist {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
margin: 16px 0;
|
||||
width: 100%;
|
||||
|
||||
tr {
|
||||
background: unset;
|
||||
|
||||
& > td:last-child,
|
||||
& > th:last-child {
|
||||
width: 20%; text-align: center;
|
||||
}
|
||||
|
||||
&:nth-child(4n+2),
|
||||
&:nth-child(4n+3) {
|
||||
background: rgba(0, 0, 0, .05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Topic table */
|
||||
&.topiclist {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
|
||||
tr {
|
||||
& > *:nth-child(n+2) {
|
||||
/* This matches all children except the first column */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& > td:last-child,
|
||||
& > th:last-child {
|
||||
width: 20%; text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Thread table */
|
||||
&.thread {
|
||||
width: 100%;
|
||||
border-width: 1px 0;
|
||||
|
||||
&.topcomment {
|
||||
border: none;
|
||||
|
||||
td.message {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
div.info {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
|
||||
&.author {
|
||||
width: 256px;
|
||||
|
||||
@media screen and (max-width: @small) {
|
||||
/* Includes padding */
|
||||
width: 136px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media screen and (max-width: @tiny) {
|
||||
width: 104px;
|
||||
}
|
||||
}
|
||||
|
||||
&.message {
|
||||
padding-top: 8px;
|
||||
|
||||
& > *:nth-child(2) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.topcomment) div.info {
|
||||
float: right;
|
||||
}
|
||||
div.info {
|
||||
text-align: right;
|
||||
position: relative;
|
||||
|
||||
@media screen and (max-width: @small) {
|
||||
float: none;
|
||||
}
|
||||
|
||||
& > * {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.topcomment-placeholder div {
|
||||
font-style: italic;
|
||||
opacity: 0.5;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
box-shadow: var(--shadow);
|
||||
border-radius: 4px;
|
||||
transition: none;
|
||||
|
||||
background: var(--background);
|
||||
z-index: 2;
|
||||
border: 1px solid var(--border);
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
background: var(--background-light);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Tables with filters */
|
||||
&.filter-target th:after {
|
||||
content: attr(data-filter);
|
||||
display: block;
|
||||
font-size: 80%;
|
||||
font-family: monospace;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/* Currently 3 screen sizes supported :
|
||||
- micro: < 500px
|
||||
- tiny: < 850px
|
||||
- small: < 1200px
|
||||
- normal: >= 1200px
|
||||
|
||||
Ex:
|
||||
@media screen and (max-width: @var)
|
||||
*/
|
||||
|
||||
@micro: 499px;
|
||||
@tiny: 849px;
|
||||
@small: 1199px;
|
||||
@normal: 1449px;
|
|
@ -0,0 +1,162 @@
|
|||
@import "vars";
|
||||
|
||||
/* Profile summaries */
|
||||
|
||||
.profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 265px;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.profile-title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.profile-points {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.profile-points span {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.profile-points-small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-xp {
|
||||
height: 10px;
|
||||
min-width: 96px;
|
||||
max-width: 96px;
|
||||
background: var(--background);
|
||||
border: var(--border);
|
||||
}
|
||||
|
||||
.profile-xp-100 {
|
||||
background: var(--background-xp);
|
||||
border: var(--border-xp);
|
||||
}
|
||||
|
||||
.profile-xp div {
|
||||
height: 10px;
|
||||
background: var(--background-xp);
|
||||
border: var(--border-xp);
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
.profile-xp-100 div {
|
||||
background: var(--background-xp-100);
|
||||
}
|
||||
|
||||
.profile.guest {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile.guest em {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: @small) {
|
||||
table.thread {
|
||||
.profile {
|
||||
flex-direction: column;
|
||||
width: 96px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
order: 1;
|
||||
margin-right: 0;
|
||||
margin-top: 4px;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
.profile-title,
|
||||
.profile-points,
|
||||
.profile-xp {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-points-small {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: @tiny) {
|
||||
table.thread .profile {
|
||||
width: 96px;
|
||||
}
|
||||
table.thread .profile-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Trophies */
|
||||
.trophies {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.trophy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 260px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
border: 1px solid #c5c5c5;
|
||||
border-left: 5px solid var(--links);
|
||||
border-radius: 2px;
|
||||
|
||||
&.disabled {
|
||||
filter: grayscale(100%);
|
||||
opacity: .5;
|
||||
border-left: 1px solid #c5c5c5;
|
||||
}
|
||||
|
||||
&.form-disabled {
|
||||
border-left: 1px solid #c5c5c5;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 48px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
div > * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
hr.signature {
|
||||
opacity: 0.2;
|
||||
}
|
|
@ -11,3 +11,17 @@ function getCookie(name) {
|
|||
if( end == -1 ) end = document.cookie.length;
|
||||
return unescape( document.cookie.substring( debut+name.length+1, end ) );
|
||||
}
|
||||
|
||||
/* Automatically close context menus when clicking out of them */
|
||||
function closeContextMenus(e) {
|
||||
document.querySelectorAll('details[open]>.context-menu').forEach(menu => {
|
||||
if(!menu.contains(event.target)) {
|
||||
menu.parentElement.open = false;
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
(function(){
|
||||
window.addEventListener("click", closeContextMenus);
|
||||
})();
|
||||
|
|
|
@ -89,6 +89,16 @@
|
|||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.theme.label }}
|
||||
{% for subfield in form.theme %}
|
||||
<div>{{ subfield }} {{ subfield.label }}</div>
|
||||
{% endfor %}
|
||||
<div class=desc>{{ form.theme.description }}</div>
|
||||
{% for error in form.theme.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-ok") }}</div>
|
||||
</form>
|
||||
|
||||
|
|
|
@ -6,6 +6,13 @@
|
|||
<form action="{{ url_for('delete_account') }}" method="post">
|
||||
{{ del_form.hidden_tag() }}
|
||||
<div>
|
||||
{{ del_form.transfer.label }}
|
||||
{{ del_form.transfer() }}
|
||||
<div style="font-size: 80%; color: gray">{{ del_form.transfer.description }}</div>
|
||||
{% for error in del_form.transfer.errors %}
|
||||
<span class=msgerror>{{ error }}</span>
|
||||
{% endfor %}
|
||||
<br>
|
||||
{{ del_form.delete.label }}
|
||||
{{ del_form.delete(checked=False) }}
|
||||
<div style="font-size:80%;color:rgba(0,0,0,.5)">{{ del_form.delete.description }}</div>
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
<h1>Créer un sondage</h1>
|
||||
<form action="" method="post">
|
||||
<div>
|
||||
{{ form.title.label }}<br>
|
||||
{{ form.title(size=32) }}<br>
|
||||
{{ form.title.label }}
|
||||
{{ form.title(size=32) }}
|
||||
{% for error in form.title.errors %}
|
||||
<span style="color: red;">{{ error }}</span>
|
||||
{% endfor %}
|
||||
|
@ -24,8 +24,8 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.type.label }}<br>
|
||||
{{ form.type }}<br>
|
||||
{{ form.type.label }}
|
||||
{{ form.type }}
|
||||
{% for error in form.type.errors %}
|
||||
<span style="color: red;">{{ error }}</span>
|
||||
{% endfor %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
{% if current_user.is_authenticated %}
|
||||
{% if current_user == member %}
|
||||
<div><a href="{{ url_for('edit_account') }}">Modifier le compte</a></div>
|
||||
{% elif current_user.priv('access-admin-panel') %}
|
||||
{% elif current_user.priv('edit.accounts') %}
|
||||
<div><a href="{{ url_for('adm_edit_account', user_id=member.id) }}">Modifier le compte</a></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -18,28 +18,9 @@
|
|||
<td>{{ a.id }}</td>
|
||||
<td><a href="{{ a.url }}">{{ a.name }}</a></td>
|
||||
<td><a href="{{ url_for('user', username=a.comment.author.name) }}">{{ a.comment.author.name }}</a></td>
|
||||
<td>{{ a.size }}</td>
|
||||
<td>{{ a.size | humanize(unit='o') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h2>Liste des groupes</h2>
|
||||
|
||||
<table style="width:90%; margin: auto;">
|
||||
<tr><th>Groupe</th><th>Membres</th><th>Privilèges</th></tr>
|
||||
|
||||
{% for group in groups %}
|
||||
<tr><td><span style="{{ group.css }}">{{ group.name }}</span></td><td>
|
||||
{% for user in group.members %}
|
||||
{{ user.name }}
|
||||
{% endfor %}
|
||||
</td><td>
|
||||
{% for priv in group.privs() %}
|
||||
<code>{{ priv }}</code>
|
||||
{{- ', ' if not loop.last }}
|
||||
{% endfor %}
|
||||
</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -7,16 +7,29 @@
|
|||
{% block content %}
|
||||
<section class="form">
|
||||
<h2>Confirmer la suppression du compte</h2>
|
||||
<p>Le compte '{{ user.name }}' que vous allez supprimer est lié à :</p>
|
||||
<p>Le compte '{{ user.name }}' possède les posts (migrables) suivants :</p>
|
||||
<ul>
|
||||
<li>{{ user.groups | length }} groupe{{ user.groups|length|pluralize }}</li>
|
||||
<li>{% set sp = user.special_privileges() | length %}
|
||||
{{- sp }} privilège{{sp|pluralize}} spéci{{sp|pluralize("al","aux")}}</li>
|
||||
<li>{{ stats.comments }} commentaire{{ stats.comments | pluralize }}</li>
|
||||
<li>{{ stats.topics }} topic{{ stats.topics | pluralize }}</li>
|
||||
<li>{{ stats.programs }} topic{{ stats.programs | pluralize }}</li>
|
||||
</ul>
|
||||
<p>Les propriétés suivantes seront supprimées :</p>
|
||||
<ul>
|
||||
<li>{{ stats.groups }} groupe{{ stats.groups | pluralize }}</li>
|
||||
<li>{{ stats.privs }} privilège{{ stats.privs | pluralize }}
|
||||
spéci{{ stats.privs | pluralize("al","aux") }}</li>
|
||||
</ul>
|
||||
|
||||
<form action="{{ url_for('adm_delete_account', user_id=user.id) }}" method=post>
|
||||
{{ del_form.hidden_tag() }}
|
||||
<div>
|
||||
{{ del_form.transfer.label }}
|
||||
{{ del_form.transfer() }}
|
||||
<div style="font-size: 80%; color: gray">{{ del_form.transfer.description }}</div>
|
||||
{% for error in del_form.transfer.errors %}
|
||||
<span class=msgerror>{{ error }}</span>
|
||||
{% endfor %}
|
||||
<br>
|
||||
{{ del_form.delete.label }}
|
||||
{{ del_form.delete(checked=False) }}
|
||||
<div style="font-size: 80%; color: gray">{{ del_form.delete.description }}</div>
|
||||
|
|
|
@ -40,6 +40,9 @@
|
|||
{{ form.email_confirmed.label }}
|
||||
{{ form.email_confirmed(checked=user.email_confirmed) }}
|
||||
<div class=desc>{{ form.email_confirmed.description }}</div>
|
||||
{% for error in form.email_confirmed.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.password.label }}
|
||||
|
@ -107,15 +110,16 @@
|
|||
<form action="{{ url_for('adm_edit_account', user_id=user.id) }}" method="post">
|
||||
{{ trophy_form.hidden_tag() }}
|
||||
<h2>Trophées</h2>
|
||||
<div class="trophies-panel flex-grid fg3">
|
||||
{% for id, input in trophy_form.__dict__.items() %}
|
||||
{% if id[0] == "t" %}
|
||||
<div>
|
||||
{# TODO: add trophies icons #}
|
||||
{{ input(checked=id in trophies_owned) }}
|
||||
{{ input.label }}
|
||||
<div class="trophies">
|
||||
{% for id, t in trophy_form.trophies.items() %}
|
||||
<div class="trophy form-disabled">
|
||||
{{ trophy_form[id](checked=id in trophies_owned) }}
|
||||
<img src="{{ url_for('static', filename='images/trophies/'+slugify(t.name))+'.png' }}">
|
||||
<div>
|
||||
<em>{{ t.name }}</em>
|
||||
<span>{{ t.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ trophy_form.submit(class_="bg-ok") }}</div>
|
||||
|
@ -127,14 +131,11 @@
|
|||
{{ group_form.hidden_tag() }}
|
||||
<h2>Groupes</h2>
|
||||
<div class="groups-panel flex-grid fg3">
|
||||
{% for id, input in group_form.__dict__.items() %}
|
||||
{% if id[0] == "g" %}
|
||||
{% for id, g in group_form.groups.items() %}
|
||||
<div>
|
||||
{# TODO: add trophies icons #}
|
||||
{{ input(checked=id in groups_owned) }}
|
||||
{{ input.label }}
|
||||
{{ group_form[id](checked=id in groups_owned) }}
|
||||
{{ group_form[id].label(style=g.css) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ group_form.submit(class_="bg-ok") }}</div>
|
||||
|
|
|
@ -12,8 +12,10 @@
|
|||
<li><a href="{{ url_for('adm_members') }}">Liste des membres</a></li>
|
||||
<li><a href="{{ url_for('adm_trophies') }}">Titres et trophées</a></li>
|
||||
<li><a href="{{ url_for('adm_forums') }}">Arbre des forums</a></li>
|
||||
<li><a href="{{ url_for('adm_polls') }}">Sondages</a></li>
|
||||
<li><a href="{{ url_for('adm_attachments') }}">Pièces-jointes</a></li>
|
||||
<li><a href="{{ url_for('adm_config') }}">Configuration du site</a></li>
|
||||
<li><a href="{{ url_for('adm_login_as') }}">Vandalisme</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<h1>Vandaliser un compte</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form">
|
||||
<form action="" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.username.label }}<br>
|
||||
{{ form.username(size=32) }}<br>
|
||||
{% for error in form.username.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>{{ form.submit(class_="bg-ok") }}</p>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -3,6 +3,7 @@
|
|||
<html lang="fr-FR">
|
||||
{% include "base/head.html" %}
|
||||
<body>
|
||||
<div><a class="skip-to-content-link" href="#main-content">Aller directement au contenu</a></div>
|
||||
{% include "base/navbar.html" %}
|
||||
|
||||
<div class=container>
|
||||
|
@ -10,8 +11,10 @@
|
|||
<div class=title>{% block title %}<h1>Planète Casio</h1>{% endblock %}</div>
|
||||
{% include "base/header.html" %}
|
||||
</header>
|
||||
|
||||
{% include "base/flash.html" %}
|
||||
|
||||
<div id="main-content"></div>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<footer>
|
||||
<p>Planète Casio est un site communautaire non affilié à CASIO. Toute reproduction de Planète Casio, même partielle, est interdite.</p>
|
||||
<p>Les programmes et autres publications présentes sur Planète Casio restent la propriété de leurs auteurs et peuvent être soumis à des licences ou des copyrights.</p>
|
||||
{% if current_user.is_authenticated and current_user.priv('footer-statistics') %}
|
||||
<p>Page générée en {{ g.request_time() }}</p>
|
||||
{% if current_user.is_authenticated and current_user.priv('misc.dev-infos') %}
|
||||
<p>Page générée en {{ "%.3f" % g.request_time() }} secondes.</p>
|
||||
{% endif %}
|
||||
<p>Ceci est un environnement de test. Tout contenu peut être supprimé sans avertissement préalable.</p>
|
||||
</footer>
|
||||
|
|
|
@ -9,5 +9,4 @@
|
|||
{% for s in styles %}
|
||||
<link rel="stylesheet" type="text/css" href={{url_for('static', filename = s)}}>
|
||||
{% endfor %}
|
||||
<link rel="stylesheet" media="all and (max-width: 699px)" type="text/css" href={{url_for('static', filename = 'css/light.css')}}>
|
||||
</head>
|
||||
</head>
|
||||
|
|
|
@ -11,3 +11,10 @@
|
|||
<div id="spotlight">
|
||||
<a href="#" class="button bg-error">Jeu du mois : février 2019</a>
|
||||
</div>
|
||||
|
||||
{% if current_user.is_authenticated and current_user.priv('misc.dev-infos') %}
|
||||
{% set reqtime = g.request_time() %}
|
||||
{% if reqtime > V5Config.SLOW_REQUEST_THRESHOLD %}
|
||||
<div id="server-speed-warning">Génération<br>{{ "%.3f" % reqtime }} s</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<path fill="#ffffff" d="M12,15.39L8.24,17.66L9.23,13.38L5.91,10.5L10.29,10.13L12,6.09L13.71,10.13L18.09,10.5L14.77,13.38L15.76,17.66M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z"></path>
|
||||
</svg>Topics favoris
|
||||
</a>
|
||||
{% if current_user.priv('access-admin-panel') %}
|
||||
{% if current_user.priv('misc.admin-panel') %}
|
||||
<a href="{{ url_for('adm') }}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M3,1H19A1,1 0 0,1 20,2V6A1,1 0 0,1 19,7H3A1,1 0 0,1 2,6V2A1,1 0 0,1 3,1M3,9H19A1,1 0 0,1 20,10V10.67L17.5,9.56L11,12.44V15H3A1,1 0 0,1 2,14V10A1,1 0 0,1 3,9M3,17H11C11.06,19.25 12,21.4 13.46,23H3A1,1 0 0,1 2,22V18A1,1 0 0,1 3,17M8,5H9V3H8V5M8,13H9V11H8V13M8,21H9V19H8V21M4,3V5H6V3H4M4,11V13H6V11H4M4,19V21H6V19H4M17.5,12L22,14V17C22,19.78 20.08,22.37 17.5,23C14.92,22.37 13,19.78 13,17V14L17.5,12M17.5,13.94L15,15.06V17.72C15,19.26 16.07,20.7 17.5,21.06V13.94Z"></path>
|
||||
|
@ -41,11 +41,19 @@
|
|||
<path fill="#ffffff" d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"></path>
|
||||
</svg>Paramètres
|
||||
</a>
|
||||
{% if is_vandal() %}
|
||||
<a href="{{ url_for('adm_logout_as', csrf_token=csrf_token()) }}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M17,17.25V14H10V10H17V6.75L22.25,12L17,17.25M13,2A2,2 0 0,1 15,4V8H13V4H4V20H13V16H15V20A2,2 0 0,1 13,22H4A2,2 0 0,1 2,20V4A2,2 0 0,1 4,2H13Z"></path>
|
||||
</svg>Fuir ce compte
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('logout', csrf_token=csrf_token()) }}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M17,17.25V14H10V10H17V6.75L22.25,12L17,17.25M13,2A2,2 0 0,1 15,4V8H13V4H4V20H13V16H15V20A2,2 0 0,1 13,22H4A2,2 0 0,1 2,20V4A2,2 0 0,1 4,2H13Z"></path>
|
||||
</svg>Déconnexion
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
|
|
|
@ -10,7 +10,10 @@
|
|||
<hr>
|
||||
|
||||
{% for f in main_forum.sub_forums %}
|
||||
<a href="{{ url_for('forum_page', f=f) }}">{{ f.name }}</a>
|
||||
{% if f.is_default_accessible() or
|
||||
(current_user.is_authenticated and current_user.can_access_forum(f)) %}
|
||||
<a href="{{ url_for('forum_page', f=f) }}">{{ f.name }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<hr>
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
<h1>Édition de commentaire</h1>
|
||||
|
||||
<h3>Commentaire actuel</h3>
|
||||
<table class="thread">
|
||||
<table class="thread topcomment">
|
||||
<tr>
|
||||
<td class="author">{{ widget_user.profile(comment.author) }}</td>
|
||||
<td><div>{{ comment.text }}</div></td>
|
||||
<td><div>{{ comment.text | md }}</div></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
@ -24,15 +24,35 @@
|
|||
{{ form.hidden_tag() }}
|
||||
|
||||
{% if form.pseudo %}
|
||||
{{ form.pseudo.label }}
|
||||
{{ form.pseudo }}
|
||||
{% for error in form.pseudo.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
<div>
|
||||
{{ form.pseudo.label }}
|
||||
{{ form.pseudo }}
|
||||
{% for error in form.pseudo.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ widget_editor.text_editor(form.message, label=False, autofocus=True) }}
|
||||
|
||||
{% if form.attachment_list %}
|
||||
<div>Supprimer des pièces jointes<br>
|
||||
{% for id, a in form.attachment_list.items() %}
|
||||
{{ form[id]() }} <code>{{ a.name }}</code> ({{ a.size }} octets)<br>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
{{ form.attachments.label }}
|
||||
<div>
|
||||
{{ form.attachments }}
|
||||
{% for error in form.attachments.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>{{ form.submit(class_='bg-ok') }}</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,19 +1,82 @@
|
|||
{% extends "base/base.html" %}
|
||||
{% import "widgets/attachments.html" as widget_attachments %}
|
||||
{% import "widgets/thread.html" as widget_thread with context %}
|
||||
{% import "widgets/editor.html" as widget_editor %}
|
||||
{% import "widgets/member.html" as widget_member %}
|
||||
{% import "widgets/user.html" as widget_user %}
|
||||
|
||||
{% block title %}
|
||||
<a href='/forum'>Forum de Planète Casio</a> » <a href="{{ url_for('forum_page', f=t.forum) }}">{{ t.forum.name }}</a> » <h1>{{ t.title }}</h1>
|
||||
<a href='/forum'>Forum de Planète Casio</a> » <a href="{{ url_for('forum_page', f=t.forum) }}">{{ t.forum.name }}</a> » <h1>Édition de sujet</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>Édition du topic {{ t.title }}</h1>
|
||||
<h1>Édition du sujet: {{ t.title }}</h1>
|
||||
|
||||
<div class=form>
|
||||
<h3>Commenter le sujet</h3>
|
||||
<h3>Sujet actuel</h3>
|
||||
|
||||
<p><i>Pour modifier substantiellement ou réécrire le commentaire d'en-tête, il vaut mieux poster un nouveau commentaire et le désigner comme en-tête ; ça permet à la conversation de rester dans son contexte.</i></p>
|
||||
|
||||
{% call widget_thread.thread_leader(t.thread.top_comment) %}
|
||||
<div class="info">
|
||||
<div>Posté le {{ t.date_created | dyndate }}</div>
|
||||
{{ widget_thread.post_actions(t) }}
|
||||
</div>
|
||||
{{ t.thread.top_comment.text | md }}
|
||||
{{ widget_attachments.attachments(t.thread.top_comment) }}
|
||||
{% endcall %}
|
||||
|
||||
<div class="form">
|
||||
<h3>Nouveau sujet</h3>
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
Un formulaire
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div>
|
||||
{{ form.forum.label }}
|
||||
{{ form.forum }}
|
||||
{% for error in form.forum.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.title.label }}
|
||||
{{ form.title }}
|
||||
{% for error in form.title.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if form.pseudo %}
|
||||
<div>
|
||||
{{ form.pseudo.label }}
|
||||
{{ form.pseudo }}
|
||||
{% for error in form.pseudo.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ widget_editor.text_editor(form.message, label=False, autofocus=True) }}
|
||||
|
||||
{% if form.attachment_list %}
|
||||
<div>Supprimer des pièces jointes<br>
|
||||
{% for id, a in form.attachment_list.items() %}
|
||||
{{ form[id]() }} <code>{{ a.name }}</code> ({{ a.size }} octets)<br>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
{{ form.attachments.label }}
|
||||
<div>
|
||||
{{ form.attachments }}
|
||||
{% for error in form.attachments.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>{{ form.submit(class_='bg-ok') }}</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<tr><td><a href='{{ url_for('forum_topic', f=t.forum, page=(t,1)) }}'>{{ t.title }}</a></td>
|
||||
<td><a href='{{ url_for('user', username=t.author.name) }}'>{{ t.author.name }}</a></td>
|
||||
<td>{{ t.date_created | date }}</td>
|
||||
<td>{{ t.thread.comments.count() }}</td>
|
||||
<td>{{ comment_counts[t.thread.id] }}</td>
|
||||
<td>{{ t.views }} </td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
@ -48,10 +48,8 @@
|
|||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if (current_user.is_authenticated and current_user.priv('write-anywhere'))
|
||||
or ("/actus" in f.url and current_user.is_authenticated and current_user.priv('write-news'))
|
||||
or ("/actus" not in f.url and not f.sub_forums)
|
||||
and (current_user.is_authenticated or V5Config.ENABLE_GUEST_POST) %}
|
||||
{% if (V5Config.ENABLE_GUEST_POST and f.is_default_postable())
|
||||
or (current_user.is_authenticated and current_user.can_post_in_forum(f)) %}
|
||||
<div class=form>
|
||||
<h2>Créer un nouveau sujet</h2>
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
|
@ -64,6 +62,7 @@
|
|||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ form.ab }}
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
|
@ -76,10 +75,15 @@
|
|||
|
||||
{{ widget_editor.text_editor(form.message) }}
|
||||
|
||||
{{ form.attachments }}
|
||||
{% for error in form.attachments.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
<div>
|
||||
{{ form.attachments.label }}
|
||||
<div>
|
||||
{{ form.attachments }}
|
||||
{% for error in form.attachments.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>{{ form.submit(class_='bg-ok') }}</div>
|
||||
</form>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<p>
|
||||
Bienvenue sur le forum de Planète Casio ! Vous pouvez créer des
|
||||
nouveaux sujets ou poster des réponses avec un compte
|
||||
{% if not current_user.is_authenticated %}
|
||||
{%- if not current_user.is_authenticated %}
|
||||
ou en postant en tant qu'invité
|
||||
{% endif %}
|
||||
{%- endif -%}
|
||||
.
|
||||
</p>
|
||||
|
||||
|
@ -20,22 +20,28 @@
|
|||
{% else %}
|
||||
|
||||
{% for l1 in main_forum.sub_forums %}
|
||||
<table class=forumlist>
|
||||
<tr><th>{{ l1.name }}</th><th>Nombre de sujets</th></tr>
|
||||
{% if l1.is_default_accessible() or
|
||||
(current_user.is_authenticated and current_user.can_access_forum(l1)) %}
|
||||
<table class=forumlist>
|
||||
<tr><th>{{ l1.name }}</th><th>Nombre de sujets</th></tr>
|
||||
|
||||
{% if l1.sub_forums == [] %}
|
||||
<tr><td><a href='/forum{{ l1.url }}'>{{ l1.name }}</a></td>
|
||||
<td>{{ l1.topics.count() }}</td></tr>
|
||||
<tr><td>{{ l1.descr }}</td><td></td></tr>
|
||||
{% endif %}
|
||||
{% if l1.sub_forums == [] %}
|
||||
<tr><td><a href='/forum{{ l1.url }}'>{{ l1.name }}</a></td>
|
||||
<td>{{ l1.topics.count() }}</td></tr>
|
||||
<tr><td>{{ l1.descr }}</td><td></td></tr>
|
||||
{% endif %}
|
||||
|
||||
{% for l2 in l1.sub_forums %}
|
||||
<tr><td><a href='/forum{{ l2.url }}'>{{ l2.name }}</td>
|
||||
<td>{{ l2.topics.count() }}</td></tr>
|
||||
<tr><td>{{ l2.descr }}</td><td></td></tr>
|
||||
{% endfor %}
|
||||
{% for l2 in l1.sub_forums %}
|
||||
{% if l2.is_default_accessible() or
|
||||
(current_user.is_authenticated and current_user.can_access_forum(l2)) %}
|
||||
<tr><td><a href='/forum{{ l2.url }}'>{{ l2.name }}</td>
|
||||
<td>{{ l2.topics.count() }}</td></tr>
|
||||
<tr><td>{{ l2.descr }}</td><td></td></tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
{% extends "base/base.html" %}
|
||||
{% import "widgets/editor.html" as widget_editor %}
|
||||
{% import "widgets/thread.html" as widget_thread %}
|
||||
{% import "widgets/thread.html" as widget_thread with context %}
|
||||
{% import "widgets/user.html" as widget_user %}
|
||||
{% import "widgets/pagination.html" as widget_pagination with context %}
|
||||
{% import "widgets/attachments.html" as widget_attachments %}
|
||||
|
||||
{% block title %}
|
||||
<a href='/forum'>Forum de Planète Casio</a> » <a href="{{ url_for('forum_page', f=t.forum) }}">{{ t.forum.name }}</a> » <h1>{{ t.title }}</h1>
|
||||
|
@ -11,7 +12,17 @@
|
|||
{% block content %}
|
||||
<section>
|
||||
<h1>{{ t.title }}</h1>
|
||||
{{ widget_thread.thread([t.thread.top_comment], None) }}
|
||||
|
||||
{% if t.thread.top_comment %}
|
||||
{% call widget_thread.thread_leader(t.thread.top_comment) %}
|
||||
<div class="info">
|
||||
<div>Posté le {{ t.date_created | dyndate }}</div>
|
||||
{{ widget_thread.post_actions(t) }}
|
||||
</div>
|
||||
{{ t.thread.top_comment.text | md }}
|
||||
{{ widget_attachments.attachments(t.thread.top_comment) }}
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
{{ widget_pagination.paginate(comments, 'forum_topic', t, {'f': t.forum}) }}
|
||||
|
||||
|
@ -25,30 +36,39 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated or V5Config.ENABLE_GUEST_POST %}
|
||||
{% if V5Config.ENABLE_GUEST_POST
|
||||
or (current_user.is_authenticated and current_user.can_post_in_forum(t.forum)) %}
|
||||
<div class=form>
|
||||
<h3>Commenter le sujet</h3>
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{% if form.pseudo %}
|
||||
<div>
|
||||
{{ form.pseudo.label }}
|
||||
{{ form.pseudo }}
|
||||
{% for error in form.pseudo.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
{{ form.ab }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ widget_editor.text_editor(form.message, label=False) }}
|
||||
|
||||
{{ form.attachments }}
|
||||
{% for error in form.attachments.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
<div>
|
||||
{{ form.attachments.label }}
|
||||
<div>
|
||||
{{ form.attachments }}
|
||||
{% for error in form.attachments.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>{{ form.submit(class_='bg-ok') }}</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{% for a in comment.attachments %}
|
||||
<tr>
|
||||
<td><a href="{{ a.url }}">{{ a.name }}</a></td>
|
||||
<td>{{ a.size }}</td>
|
||||
<td>{{ a.size | humanize(unit='o') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
|
|
@ -1,25 +1,68 @@
|
|||
{% import "widgets/user.html" as widget_user %}
|
||||
{% import "widgets/attachments.html" as widget_attachments %}
|
||||
|
||||
{% macro thread(comments, top_comment) %}
|
||||
<table class="thread {{ 'topcomment' if top_comment == None else ''}} ">
|
||||
{% for c in comments %}
|
||||
<tr id="{{ c.id }}">
|
||||
{% if c != top_comment %}
|
||||
<td class="author">{{ widget_user.profile(c.author) }}</td>
|
||||
<td>
|
||||
<div class="info">
|
||||
<div>Posté le {{ c.date_created|date }}</div>
|
||||
{% if c.date_created != c.date_modified %}
|
||||
<div>Modifié le {{ c.date_modified|date }}</div>
|
||||
{# Post actions: this widget expands to a context menu with actions controlling
|
||||
a post, supporting different types of posts. #}
|
||||
{% macro post_actions(post) %}
|
||||
{# TODO (Guest edit): determine permissions in post_actions widget #}
|
||||
|
||||
{% set auth = current_user.is_authenticated %}
|
||||
{% set can_edit = auth and current_user.can_edit_post(post) %}
|
||||
{% set can_delete = auth and current_user.can_delete_post(post) %}
|
||||
{% set can_punish = auth and current_user.can_punish_post(post) %}
|
||||
{% set can_topcomm = auth and current_user.can_set_topcomment(post) %}
|
||||
|
||||
{% if post.type == "topic" %}
|
||||
{% set suffix = " le sujet" %}
|
||||
{% elif post.type == "program" %}
|
||||
{% set suffix = " le programme" %}
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit or can_delete or can_punish or can_topcomm %}
|
||||
<details>
|
||||
<summary><b>⋮</b></summary>
|
||||
<div class='context-menu'>
|
||||
{% if can_edit %}
|
||||
<a href="{{ url_for('edit_post', postid=post.id, r=request.path) }}">Modifier{{ suffix }}</a>
|
||||
{% endif %}
|
||||
<div><a href="{{ request.path }}#{{ c.id }}">Permalink</a></div>
|
||||
<div><a href="{{ url_for('edit_post', postid=c.id, r=request.path) }}">Modifier</a></div>
|
||||
<div><a href="{{ url_for('delete_post', postid=c.id, csrf_token=csrf_token()) }}" onclick="return confirm('Le message sera supprimé')">Supprimer</a></div>
|
||||
|
||||
{% if can_punish %}
|
||||
<a href="{{ url_for('delete_post', postid=post.id, penalty=False, csrf_token=csrf_token()) }}" onclick="return confirm('Le post sera supprimé.')">Supprimer{{ suffix }} (normal)</a>
|
||||
<a href="{{ url_for('delete_post', postid=post.id, penalty=True, csrf_token=csrf_token()) }}" onclick="return confirm('Le post sera supprimé avec pénalité d\'XP.')">Supprimer{{ suffix }} (pénalité)</a>
|
||||
{% elif can_delete %}
|
||||
<a href="{{ url_for('delete_post', postid=post.id, penalty=False, csrf_token=csrf_token()) }}" onclick="return confirm('Le post sera supprimé !')">Supprimer{{ suffix }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if can_topcomm %}
|
||||
<a href="{{ url_for('set_post_topcomment', postid=post.id, csrf_token=csrf_token()) }}">Utiliser comme en-tête</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{# Thread widget: this widget expands to a table that shows a list of comments
|
||||
from a thread, along with message controls.
|
||||
|
||||
comments: List of comments to render
|
||||
top_comment: Thread's top comment (will be elided if encountered) #}
|
||||
|
||||
{% macro thread(comments, top_comment, owner=None) %}
|
||||
<table class="thread">
|
||||
{% for c in comments %}
|
||||
{% if c != top_comment %}
|
||||
<tr id="{{ c.id }}">
|
||||
<td class="author">{{ widget_user.profile(c.author) }}</td>
|
||||
<td class="message">
|
||||
<div class="info">
|
||||
<div>Posté le <a href="{{ request.path }}#{{ c.id }}">{{ c.date_created | dyndate }}</a></div>
|
||||
{% if c.date_created != c.date_modified %}
|
||||
<div>Modifié le <a href="{{ request.path }}#{{ c.id }}">{{ c.date_modified | dyndate }}</a></div>
|
||||
{% endif %}
|
||||
{{ post_actions(c) }}
|
||||
</div>
|
||||
|
||||
{{ c.text|md }}
|
||||
|
||||
{{ widget_attachments.attachments(c) }}
|
||||
|
||||
{% if c.author.signature %}
|
||||
|
@ -27,10 +70,30 @@
|
|||
{{ c.author.signature|md }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% elif loop.index0 != 0 %}
|
||||
<div>Ce message est le top comment</div>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% elif loop.index0 != 0 %}
|
||||
<tr class="topcomment-placeholder">
|
||||
<td></td>
|
||||
<td><div>Le commentaire à cet endroit est actuellement utilisé comme en-tête.</div></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{# Thread leader widget: this widget expands to a single-message thread which
|
||||
can show more text when called. This is intended for programs and similar
|
||||
objects which display metadata before description and comments.
|
||||
|
||||
leader: Posts's top comment (actual rendering is delegated to caller) #}
|
||||
|
||||
{% macro thread_leader(leader) %}
|
||||
<table class="thread topcomment">
|
||||
{# Empty line to get normal background (instead of alternate one) #}
|
||||
<tr></tr>
|
||||
<tr id="{{ leader.id }}">
|
||||
<td class="author">{{ widget_user.profile(leader.author) }}</td>
|
||||
<td class="message">{{ caller() }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue