Compare commits

...

No commits in common. "0.x" and "master" have entirely different histories.
0.x ... master

854 changed files with 58362 additions and 26468 deletions

5
.gitattributes vendored Normal file
View file

@ -0,0 +1,5 @@
# Auto detect text files and perform LF normalization
* text=auto
# Shell scripts require LF
*.sh text eol=lf

108
.github/workflows/build-containers.yml vendored Normal file
View file

@ -0,0 +1,108 @@
name: Build Docker containers
on:
push:
branches:
- master
jobs:
build-client:
name: Build and push client/ Docker container
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Determine metadata
run: |
CLOSEST_VER="$(git describe --tags --abbrev=0 $GITHUB_SHA)"
CLOSEST_MAJOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f1)"
CLOSEST_MINOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f2)"
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c1-8)
BUILD_INFO="v${CLOSEST_VER}-${SHORT_COMMIT}"
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
echo "major_tag=${CLOSEST_MAJOR_VER}" >> $GITHUB_ENV
echo "minor_tag=${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}" >> $GITHUB_ENV
echo "build_info=${BUILD_INFO}" >> $GITHUB_ENV
echo "build_date=${BUILD_DATE}" >> $GITHUB_ENV
echo "Build Info: ${BUILD_INFO}"
echo "Build Date: ${BUILD_DATE}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build container
run: >
docker buildx build --push
--platform linux/amd64,linux/arm/v7,linux/arm64/v8
--build-arg BUILD_INFO=${{ env.build_info }}
--build-arg BUILD_DATE=${{ env.build_date }}
--build-arg SOURCE_COMMIT=$GITHUB_SHA
--build-arg DOCKER_REPO=szurubooru/client
-t "szurubooru/client:latest"
-t "szurubooru/client:${{ env.major_tag }}"
-t "szurubooru/client:${{ env.minor_tag }}"
./client
build-server:
name: Build and push server/ Docker container
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Determine metadata
run: |
CLOSEST_VER="$(git describe --tags --abbrev=0 $GITHUB_SHA)"
CLOSEST_MAJOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f1)"
CLOSEST_MINOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f2)"
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c1-8)
BUILD_INFO="v${CLOSEST_VER}-${SHORT_COMMIT}"
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
echo "major_tag=${CLOSEST_MAJOR_VER}" >> $GITHUB_ENV
echo "minor_tag=${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}" >> $GITHUB_ENV
echo "build_info=${BUILD_INFO}" >> $GITHUB_ENV
echo "build_date=${BUILD_DATE}" >> $GITHUB_ENV
echo "Build Info: ${BUILD_INFO}"
echo "Build Date: ${BUILD_DATE}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build container
run: >
docker buildx build --push
--platform linux/amd64,linux/arm/v7,linux/arm64/v8
--build-arg BUILD_DATE=${{ env.build_date }}
--build-arg SOURCE_COMMIT=$GITHUB_SHA
--build-arg DOCKER_REPO=szurubooru/server
-t "szurubooru/server:latest"
-t "szurubooru/server:${{ env.major_tag }}"
-t "szurubooru/server:${{ env.minor_tag }}"
./server

28
.github/workflows/run-unit-tests.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Run unit tests
on: [push, pull_request]
jobs:
test-server:
name: Run pytest for server/
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build test container
run: >
docker buildx build --load
--platform linux/amd64 --target testing
-t test_container
./server
- name: Run unit tests
run: >
docker run --rm -t test_container
--color=no
--cov-report=term-missing:skip-covered
--cov=szurubooru
szurubooru/

18
.gitignore vendored Normal file
View file

@ -0,0 +1,18 @@
# User-specific configuration
config.yaml
.env
# Client Development Artifacts
*/*_modules/
client/public
# Server Development Artifacts
.coverage
.cache
server/**/lib/
server/**/bin/
server/**/pyvenv.cfg
__pycache__/
data/
sql/

12
.gitmodules vendored
View file

@ -1,12 +0,0 @@
[submodule "chibi-core"]
path = lib/chibi-core
url = https://github.com/rr-/chibi-core.git
[submodule "php-markdown"]
path = lib/php-markdown
url = https://github.com/michelf/php-markdown.git
[submodule "lib/chibi-sql"]
path = lib/chibi-sql
url = https://github.com/rr-/chibi-sql.git
[submodule "lib/TextCaseConverter"]
path = lib/TextCaseConverter
url = https://gist.github.com/rr-/10522533.git

62
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,62 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: mixed-line-ending
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.4.2
hooks:
- id: remove-tabs
- repo: https://github.com/psf/black
rev: '23.1.0'
hooks:
- id: black
files: 'server/'
types: [python]
language_version: python3.9
- repo: https://github.com/PyCQA/isort
rev: '5.12.0'
hooks:
- id: isort
files: 'server/'
types: [python]
exclude: server/szurubooru/migrations/env.py
additional_dependencies:
- toml
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
- id: prettier
files: client/js/
exclude: client/js/.gitignore
args: ['--config', 'client/.prettierrc.yml']
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.33.0
hooks:
- id: eslint
files: client/js/
args: ['--fix']
additional_dependencies:
- eslint-config-prettier
- repo: https://github.com/PyCQA/flake8
rev: '6.0.0'
hooks:
- id: flake8
files: server/szurubooru/
additional_dependencies:
- flake8-print
args: ['--config=server/.flake8']
fail_fast: true
exclude: LICENSE.md

20
LICENSE
View file

@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 Marcin Kurczewski
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

676
LICENSE.md Normal file
View file

@ -0,0 +1,676 @@
GNU GENERAL PUBLIC LICENSE
==========================
Version 3, 29 June 2007
==========================
> Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
# Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
# TERMS AND CONDITIONS
## 0. Definitions.
_"This License"_ refers to version 3 of the GNU General Public License.
_"Copyright"_ also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
_"The Program"_ refers to any copyrightable work licensed under this
License. Each licensee is addressed as _"you"_. _"Licensees"_ and
"recipients" may be individuals or organizations.
To _"modify"_ a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a _"modified version"_ of the
earlier work or a work _"based on"_ the earlier work.
A _"covered work"_ means either the unmodified Program or a work based
on the Program.
To _"propagate"_ a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To _"convey"_ a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
## 1. Source Code.
The _"source code"_ for a work means the preferred form of the work
for making modifications to it. _"Object code"_ means any non-source
form of a work.
A _"Standard Interface"_ means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The _"System Libraries"_ of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The _"Corresponding Source"_ for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
## 2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
## 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
## 4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
## 5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
## 6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A _"User Product"_ is either (1) a _"consumer product"_, which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
_"Installation Information"_ for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
## 7. Additional Terms.
_"Additional permissions"_ are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
## 8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
## 9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
## 10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An _"entity transaction"_ is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
## 11. Patents.
A _"contributor"_ is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's _"essential patent claims"_ are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
## 12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
## 13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
## 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
## 15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
## 16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
## 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
# END OF TERMS AND CONDITIONS
--------------------------------------------------------------------------
# How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type 'show c' for details.
The hypothetical commands _'show w'_ and _'show c'_ should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View file

@ -1,10 +1,50 @@
szurubooru
=====
# szurubooru
Szurubooru is a Danbooru-style board, a gallery where users can upload, browse, tag and comment images, video clips and flash animations.
Szurubooru is an image board engine inspired by services such as Danbooru,
Gelbooru and Moebooru dedicated for small and medium communities. Its name [has
its roots in Polish language and has onomatopeic meaning of scraping or
scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
Szurubooru is powered by [chibi](https://github.com/rr-/chibi-core), a lightweight PHP framework.
## Features
Installation instructions
-----
For installation instructions, please see the [Quick Start Guide](https://github.com/rr-/booru/wiki#quick-start-guide) on our wiki pages.
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
- Ability to retrieve web video content using [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- Post comments
- Post notes / annotations, including arbitrary polygons
- Rich JSON REST API ([see documentation](doc/API.md))
- Token based authentication for clients
- Rich search system
- Rich privilege system
- Autocomplete in search and while editing tags
- Tag categories
- Tag suggestions
- Tag implications (adding a tag automatically adds another)
- Tag aliases
- Pools and pool categories
- Duplicate detection
- Post rating and favoriting; comment rating
- Polished UI
- Browser configurable endless paging
- Browser configurable backdrop grid for transparent images
## Installation
It is recommended that you use Docker for deployment.
[See installation instructions.](doc/INSTALL.md)
More installation resources, as well as related projects can be found on the
[GitHub project Wiki](https://github.com/rr-/szurubooru/wiki)
## Screenshots
Post list:
![20160908_180032_fsk](https://cloud.githubusercontent.com/assets/1045476/18356730/3f1123d6-75ee-11e6-85dd-88a7615243a0.png)
Post view:
![20160908_180429_lmp](https://cloud.githubusercontent.com/assets/1045476/18356731/3f1566ee-75ee-11e6-9594-e86ca7347b0f.png)
## License
[GPLv3](LICENSE.md).

2
cache/.gitignore vendored
View file

@ -1,2 +0,0 @@
*
!.gitignore

1
client/.babelrc Normal file
View file

@ -0,0 +1 @@
{ "presets": ["env"] }

4
client/.dockerignore Normal file
View file

@ -0,0 +1,4 @@
node_modules/*
Dockerfile
.dockerignore
**/.gitignore

12
client/.eslintrc.yml Normal file
View file

@ -0,0 +1,12 @@
env:
browser: true
commonjs: true
es6: true
extends: 'prettier'
globals:
Atomics: readonly
SharedArrayBuffer: readonly
ignorePatterns:
- build.js
parserOptions:
ecmaVersion: 11

5
client/.jscsrc Normal file
View file

@ -0,0 +1,5 @@
{
preset: "google",
fileExtensions: [".js", "jscs"],
validateIndentation: 4,
}

4
client/.prettierrc.yml Normal file
View file

@ -0,0 +1,4 @@
parser: babel
printWidth: 79
tabWidth: 4
quoteProps: consistent

44
client/Dockerfile Normal file
View file

@ -0,0 +1,44 @@
FROM --platform=$BUILDPLATFORM node:lts as builder
WORKDIR /opt/app
COPY package.json package-lock.json ./
RUN npm install
COPY . ./
ARG BUILD_INFO="docker-latest"
ARG CLIENT_BUILD_ARGS=""
RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS}
FROM --platform=$BUILDPLATFORM scratch as approot
COPY docker-start.sh /
WORKDIR /etc/nginx
COPY nginx.conf.docker ./nginx.conf
WORKDIR /var/www
COPY --from=builder /opt/app/public/ .
FROM nginx:alpine as release
RUN apk --no-cache add dumb-init
COPY --from=approot / /
CMD ["/docker-start.sh"]
VOLUME ["/data"]
ARG DOCKER_REPO
ARG BUILD_DATE
ARG SOURCE_COMMIT
LABEL \
maintainer="" \
org.opencontainers.image.title="${DOCKER_REPO}" \
org.opencontainers.image.url="https://github.com/rr-/szurubooru" \
org.opencontainers.image.documentation="https://github.com/rr-/szurubooru/blob/${SOURCE_COMMIT}/doc/INSTALL.md" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.source="https://github.com/rr-/szurubooru" \
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
org.opencontainers.image.licenses="GPL-3.0"

424
client/build.js Executable file
View file

@ -0,0 +1,424 @@
#!/usr/bin/env node
'use strict';
// -------------------------------------------------
const webapp_icons = [
{ name: 'android-chrome-192x192.png', size: 192 },
{ name: 'android-chrome-512x512.png', size: 512 },
{ name: 'apple-touch-icon.png', size: 180 },
{ name: 'mstile-150x150.png', size: 150 }
];
const webapp_splash_screens = [
{ w: 640, h: 1136, center: 320 },
{ w: 750, h: 1294, center: 375 },
{ w: 1125, h: 2436, center: 565 },
{ w: 1242, h: 2148, center: 625 },
{ w: 1536, h: 2048, center: 770 },
{ w: 1668, h: 2224, center: 820 },
{ w: 2048, h: 2732, center: 1024 }
];
const external_js = [
'dompurify',
'js-cookie',
'marked',
'mousetrap',
'nprogress',
'superagent',
'underscore',
];
const app_manifest = {
name: 'szurubooru',
icons: [
{
src: baseUrl() + 'img/android-chrome-192x192.png',
type: 'image/png',
sizes: '192x192'
},
{
src: baseUrl() + 'img/android-chrome-512x512.png',
type: 'image/png',
sizes: '512x512'
}
],
start_url: baseUrl(),
theme_color: '#24aadd',
background_color: '#ffffff',
display: 'standalone'
}
// -------------------------------------------------
const fs = require('fs');
const glob = require('glob');
const path = require('path');
const util = require('util');
const execSync = require('child_process').execSync;
const browserify = require('browserify');
const chokidar = require('chokidar');
const WebSocket = require('ws');
var PrettyError = require('pretty-error');
var pe = new PrettyError();
function readTextFile(path) {
return fs.readFileSync(path, 'utf-8');
}
function gzipFile(file) {
file = path.normalize(file);
execSync('gzip -6 -k ' + file);
}
function baseUrl() {
return process.env.BASE_URL ? process.env.BASE_URL : '/';
}
// -------------------------------------------------
function bundleHtml() {
const underscore = require('underscore');
const babelify = require('babelify');
function minifyHtml(html) {
return require('html-minifier').minify(html, {
removeComments: true,
collapseWhitespace: true,
conservativeCollapse: true,
}).trim();
}
const baseHtml = readTextFile('./html/index.htm')
.replace('<!-- Base HTML Placeholder -->', `<base href="${baseUrl()}"/>`);
fs.writeFileSync('./public/index.htm', minifyHtml(baseHtml));
let compiledTemplateJs = [
`'use strict';`,
`let _ = require('underscore');`,
`let templates = {};`
];
for (const file of glob.sync('./html/**/*.tpl')) {
const name = path.basename(file, '.tpl').replace(/_/g, '-');
const placeholders = [];
let templateText = readTextFile(file);
templateText = templateText.replace(
/<%.*?%>/ig,
(match) => {
const ret = '%%%TEMPLATE' + placeholders.length;
placeholders.push(match);
return ret;
});
templateText = minifyHtml(templateText);
templateText = templateText.replace(
/%%%TEMPLATE(\d+)/g,
(match, number) => { return placeholders[number]; });
const functionText = underscore.template(
templateText, { variable: 'ctx' }).source;
compiledTemplateJs.push(`templates['${name}'] = ${functionText};`);
}
compiledTemplateJs.push('module.exports = templates;');
fs.writeFileSync('./js/.templates.autogen.js', compiledTemplateJs.join('\n'));
console.info('Bundled HTML');
}
function bundleCss() {
const stylus = require('stylus');
function minifyCss(css) {
return require('csso').minify(css).css;
}
let css = '';
for (const file of glob.sync('./css/**/*.styl')) {
css += stylus.render(readTextFile(file), { filename: file });
}
fs.writeFileSync('./public/css/app.min.css', minifyCss(css));
if (process.argv.includes('--gzip')) {
gzipFile('./public/css/app.min.css');
}
fs.copyFileSync(
'./node_modules/font-awesome/css/font-awesome.min.css',
'./public/css/vendor.min.css');
if (process.argv.includes('--gzip')) {
gzipFile('./public/css/vendor.min.css');
}
console.info('Bundled CSS');
}
function minifyJs(path) {
return require('terser').minify(
fs.readFileSync(path, 'utf-8'), { compress: { unused: false } }).code;
}
function writeJsBundle(b, path, compress, callback) {
let outputFile = fs.createWriteStream(path);
b.bundle().on('error', (e) => console.error(pe.render(e))).pipe(outputFile);
outputFile.on('finish', () => {
if (compress) {
fs.writeFileSync(path, minifyJs(path));
}
callback();
});
}
function bundleVendorJs(compress) {
let b = browserify();
for (let lib of external_js) {
b.require(lib);
}
if (!process.argv.includes('--no-transpile')) {
b.add(require.resolve('babel-polyfill'));
}
const file = './public/js/vendor.min.js';
writeJsBundle(b, file, compress, () => {
if (process.argv.includes('--gzip')) {
gzipFile(file);
}
console.info('Bundled vendor JS');
});
}
function bundleAppJs(b, compress, callback) {
const file = './public/js/app.min.js';
writeJsBundle(b, file, compress, () => {
if (process.argv.includes('--gzip')) {
gzipFile(file);
}
console.info('Bundled app JS');
callback();
});
}
function bundleJs() {
if (!process.argv.includes('--no-vendor-js')) {
bundleVendorJs(true);
}
if (!process.argv.includes('--no-app-js')) {
let watchify = require('watchify');
let b = browserify({ debug: process.argv.includes('--debug') });
if (!process.argv.includes('--no-transpile')) {
b = b.transform('babelify');
}
b = b.external(external_js).add(glob.sync('./js/**/*.js'));
const compress = !process.argv.includes('--debug');
bundleAppJs(b, compress, () => { });
}
}
const environment = process.argv.includes('--watch') ? "development" : "production";
function bundleConfig() {
function getVersion() {
let build_info = process.env.BUILD_INFO;
if (!build_info) {
try {
build_info = execSync('git describe --always --dirty --long --tags').toString();
} catch (e) {
console.warn('Cannot find build version');
build_info = 'unknown';
}
}
return build_info.trim();
}
const config = {
meta: {
version: getVersion(),
buildDate: new Date().toUTCString()
},
environment: environment
};
fs.writeFileSync('./js/.config.autogen.json', JSON.stringify(config));
console.info('Generated config file');
}
function bundleBinaryAssets() {
fs.copyFileSync('./img/favicon.png', './public/img/favicon.png');
console.info('Copied images');
fs.copyFileSync('./fonts/open_sans.woff2', './public/fonts/open_sans.woff2')
for (let file of glob.sync('./node_modules/font-awesome/fonts/*.*')) {
if (fs.lstatSync(file).isDirectory()) {
continue;
}
fs.copyFileSync(file, path.join('./public/fonts/', path.basename(file)));
}
if (process.argv.includes('--gzip')) {
for (let file of glob.sync('./public/fonts/*.*')) {
if (file.endsWith('woff2')) {
continue;
}
gzipFile(file);
}
}
console.info('Copied fonts')
}
function bundleWebAppFiles() {
const Jimp = require('jimp');
fs.writeFileSync('./public/manifest.json', JSON.stringify(app_manifest));
console.info('Generated app manifest');
Promise.all(webapp_icons.map(icon => {
return Jimp.read('./img/app.png')
.then(file => {
file.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.write(path.join('./public/img/', icon.name));
});
}))
.then(() => {
console.info('Generated webapp icons');
});
Promise.all(webapp_splash_screens.map(dim => {
return Jimp.read('./img/splash.png')
.then(file => {
file.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.background(0xFFFFFFFF)
.contain(dim.w, dim.center,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.contain(dim.w, dim.h,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.write(path.join('./public/img/',
'apple-touch-startup-image-' + dim.w + 'x' + dim.h + '.png'));
});
}))
.then(() => {
console.info('Generated splash screens');
});
}
function makeOutputDirs() {
const dirs = [
'./public',
'./public/css',
'./public/fonts',
'./public/img',
'./public/js'
];
for (let dir of dirs) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, 0o755);
console.info('Created directory: ' + dir);
}
}
}
function watch() {
let wss = new WebSocket.Server({ port: 8080 });
const liveReload = !process.argv.includes('--no-live-reload');
function emitReload() {
if (liveReload) {
console.log("Requesting live reload.")
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send("reload");
}
});
}
}
chokidar.watch('./fonts/**/*').on('change', () => {
try {
bundleBinaryAssets();
emitReload();
} catch (e) {
console.error(pe.render(e));
}
});
chokidar.watch('./img/**/*').on('change', () => {
try {
bundleWebAppFiles();
emitReload();
} catch (e) {
console.error(pe.render(e));
}
});
chokidar.watch('./html/**/*.tpl').on('change', () => {
try {
bundleHtml();
} catch (e) {
console.error(pe.render(e));
}
});
chokidar.watch('./css/**/*.styl').on('change', () => {
try {
bundleCss()
emitReload();
} catch (e) {
console.error(pe.render(e));
}
});
bundleBinaryAssets();
bundleWebAppFiles();
bundleCss();
bundleHtml();
bundleVendorJs(true);
let watchify = require('watchify');
let b = browserify({
debug: process.argv.includes('--debug'),
entries: ['js/main.js'],
cache: {},
packageCache: {},
});
b.plugin(watchify);
if (!process.argv.includes('--no-transpile')) {
b = b.transform('babelify');
}
b = b.external(external_js).add(glob.sync('./js/**/*.js'));
const compress = false;
function bundle(id) {
console.info("Rebundling app JS...");
let start = new Date();
bundleAppJs(b, compress, () => {
let end = new Date() - start;
console.info('Rebundled in %ds.', end / 1000)
emitReload();
});
}
b.on('update', bundle);
bundle();
}
// -------------------------------------------------
console.log("Building for '" + environment + "' environment.");
makeOutputDirs();
bundleConfig();
if (process.argv.includes('--watch')) {
watch();
} else {
if (!process.argv.includes('--no-binary-assets')) {
bundleBinaryAssets();
}
if (!process.argv.includes('--no-web-app-files')) {
bundleWebAppFiles();
}
if (!process.argv.includes('--no-html')) {
bundleHtml();
}
if (!process.argv.includes('--no-css')) {
bundleCss();
}
if (!process.argv.includes('--no-js')) {
bundleJs();
}
}

64
client/css/colors.styl Normal file
View file

@ -0,0 +1,64 @@
$main-color = #24AADD
$window-color = white
$window-color-darktheme = #1a1a1a
$top-navigation-color = #F5F5F5
$top-navigation-color-darktheme = #333333
$text-color = #111
$text-color-darktheme = #e6e6e6
$inactive-link-color = #888
$inactive-link-color-darktheme = #cccccc
$line-color = #DDD
$active-tab-background-color = rgba(0, 0, 0, 0.06)
$focused-tab-background-color = rgba(0, 0, 0, 0.03)
$active-tab-background-color-darktheme = rgba(255, 255, 255, 0.06)
$focused-tab-background-color-darktheme = rgba(255, 255, 255, 0.03)
$message-info-border-color = #BDF
$message-info-background-color = #E3EFF9
$message-error-border-color = #FCC
$message-error-background-color = #FFF5F5
$message-success-border-color = #D3E3D3
$message-success-background-color = #F5FFF5
$input-bad-border-color = #FCC
$input-bad-background-color = #FFF5F5
$input-good-border-color = #D3E3D3
$input-good-background-color = #F5FFF5
$input-enabled-background-color = #FAFAFA
$input-enabled-border-color = #EEE
$input-enabled-text-color = $text-color
$input-enabled-text-color-darktheme = $text-color-darktheme
$input-disabled-background-color = #FAFAFA
$input-disabled-border-color = #EEE
$input-disabled-text-color = #888
$button-enabled-text-color = white
$button-enabled-background-color = $main-color
$button-disabled-text-color = #666
$button-disabled-background-color = #CCC
$post-thumbnail-border-color = $main-color
$post-thumbnail-no-tags-border-color = #F44
$default-tag-category-background-color = $active-tab-background-color
$new-tag-background-color = #DFC
$new-tag-text-color = black
$implied-tag-background-color = #FFC
$implied-tag-text-color = black
$tag-suggestions-header-color = #EEE
$tag-suggestions-border-color = #AAA
$duplicate-tag-background-color = #FDC
$duplicate-tag-text-color = black
$active-note-overlay-background-color = rgba(255, 255, 255, 0.3)
$active-note-overlay-border-color = rgba(62, 255, 62, 0.8)
$note-background-color = rgba(255, 255, 205, 0.3)
$note-border-color = rgba(0, 0, 0, 0.2)
$edited-note-background-color = rgba(222, 255, 222, 0.3)
$edited-note-border-color = rgba(0, 200, 0, 0.9)
$note-text-background-color = lemonchiffon
$note-text-border-color = black
$note-text-text-color = black
$first-note-point-color = orangered
$hovered-note-point-color = red
$hovered-first-note-point-color = red
$safety-safe = #88D488
$safety-sketchy = #F3D75F
$safety-unsafe = #F3985F
$scrollbar-thumb-color = $main-color
$scrollbar-bg-color = $input-enabled-background-color
$transparency-grid-square-color = #00000000

View file

@ -0,0 +1,173 @@
@import colors
$comment-header-background-color = $top-navigation-color
$comment-header-background-color-darktheme = $top-navigation-color-darktheme
$comment-border-color = #DDD
.comment-container
padding: 0 0 0 60px
.avatar
float: left
margin-left: -60px
vertical-align: top
.thumbnail
width: 40px
height: 40px
a
display: inline-block
nav:not(.active), .tab:not(.active)
display: none
.comment
border: 1px solid $comment-border-color
header
white-space: nowrap
font-size: 95%
vertical-align: middle
position: relative
background: $comment-header-background-color
border-bottom: 1px solid $comment-border-color
nav.edit
padding: 0.25em 1em 0 1em
line-height: 2em
ul
list-style-type: none
margin: -1px 0 -1px 0
padding: 0
li
display: inline-block
border: 1px solid transparent
a
padding: 0 1em
&.active
background: $window-color
border: 1px solid $comment-border-color
border-bottom: 1px solid $window-color
nav.readonly
padding: 0 1em
line-height: 2.25em
.date, .score-container, .edit
margin-right: 2em
.score-container, .link-container
display: inline-block
&:before
position: absolute
display: block
content: ' '
width: 0
height: 0
left: -1.5em
top: calc(50% - 0.75em)
border: 0.75em solid transparent
border-right: 0.75em solid darken($comment-border-color, 10%)
&:after
position: absolute
display: block
content: ' '
width: 0
height: 0
left: calc(-1.5em + 1px)
top: calc(50% - 0.75em)
border: 0.75em solid transparent
border-right: 0.75em solid $comment-header-background-color
.edit, .delete, .score-container a, .nickname a
&:not(.inactive)
color: mix($main-color, $inactive-link-color)
i
margin-right: 0.3em
.downvote i
text-align: right
.upvote i
display: inline-block
width: 1em
margin: 0
.value
text-align: center
display: inline-block
width: 2em
.body
width: auto
margin: 1em
.keep-height
position: relative
textarea
position: absolute
width: 100%
height: 100%
.tab.edit
min-height: 150px
.messages
margin: 1em 0
.darktheme .comment-container .comment header
background: $comment-header-background-color-darktheme
nav.edit
ul
li
&.active
background: $window-color-darktheme
border-bottom: 1px solid $window-color-darktheme
.edit, .delete, .score-container a, .nickname a
&:not(.inactive)
color: mix($main-color, $inactive-link-color-darktheme)
.comment-content
p
word-wrap: normal
word-break: break-word
ul, ol
list-style-position: inside
margin: 1em 0
padding: 0 0 0 1.5em
.sjis
font-family: 'MS PGothic', ' ', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif
background: #fbfbfb
color: #111
font-size: 1em
line-height: 1
margin: 0
padding: 4px
overflow: auto
white-space: pre
word-wrap: normal
.spoiler
background: #eee
color: #eee
&:hover
color: dimgray
&:before
content: '['
color: #000
&:after
content: ']'
color: #000
blockquote
border-left: 3px solid #eee
margin-left: 0
padding: 0.3em 0.3em 0.3em 0.7em
background: #fafafa
color: #444
:first-child
margin-top: 0
:last-child
margin-bottom: 0

View file

@ -0,0 +1,9 @@
.comments>ul
list-style-type: none
margin: 0
padding: 0
>li
margin-bottom: 1em
&:last-child
margin-bottom: 0

View file

@ -0,0 +1,54 @@
@import colors
$comment-border-color = $top-navigation-color
$comment-border-color-darktheme = $top-navigation-color-darktheme
.global-comment-list
text-align: left
&>ul
list-style-type: none
margin: 1em 0 0
padding: 0
&>li
margin-top: 2em
padding-top: 2em
border-top: 3px solid $comment-border-color
&:first-child
margin-top: 0
padding-top: 0
border-top: none
@media (max-width: 700px)
.post-thumbnail
margin-bottom: 1em
.thumbnail
width: 50vw
height: 33vw
@media (min-width: 700px)
&>li
padding-left: 13em
.post-thumbnail
float: left
margin: 0 0 1em -13em
.thumbnail
width: 12em
height: 8em
&>li
clear: both
.post-thumbnail
vertical-align: top
margin-right: 1em
a
display: inline-block
.comments-container
width: 100%
.darktheme .global-comment-list
&>ul
&>li
border-top: 3px solid $comment-border-color-darktheme

386
client/css/core-forms.styl Normal file
View file

@ -0,0 +1,386 @@
@import colors
form
display: block
width: 20em
.input
list-style-type: none
margin: 0 0 2em 0
padding: 0
li
margin-top: 1.2em
label
display: block
padding: 0.3em 0
.input li:first-child label:not(.radio):not(.checkbox):not(.file-dropper),
.input li:first-child
padding-top: 0
margin-top: 0
form:not(.horizontal)
.hint
margin-top: 0.2em
margin-bottom: 0
color: $inactive-link-color
font-size: 80%
line-height: 120%
.darktheme form:not(.horizontal)
.hint
color: $inactive-link-color-darktheme
form.horizontal
display: inline-block
margin-bottom: 1em
.input, .buttons, ul
display: inline-block
vertical-align: top
margin: 0
padding: 0
input
vertical-align: top
.buttons
margin-right: 0.5em
@media (max-width: 1000px)
display: block
.input, .buttons, ul
display: block
margin-top: 0.5em
&:first-child
margin-top: 0
.buttons
margin-right: 0
/*
* Radio buttons and checkboxes
*/
input[type=radio], input[type=checkbox]
position: absolute
opacity: 0
.radio, .checkbox
box-sizing: border-box
position: relative
display: inline-block
padding-left: calc(20px + 0.5em) !important
vertical-align: middle
cursor: pointer
&:hover:before
border-color: $main-color
.radio:before
border-radius: 100%
.radio:before, .checkbox:before
transition: border-color 0.1s linear
position: absolute
left: 0
top: 0.15em
display: block
width: 16px
height: 16px
background: $input-enabled-background-color
border: 2px solid $input-enabled-border-color
content: ''
.radio:after
background: $main-color
transition: opacity 0.1s linear
position: absolute
left: 5px
top: 0.15em
margin-top: 5px
display: block
width: 10px
height: 10px
border-radius: 50%
content: ''
opacity: 0
.checkbox:after
transition: opacity 0.1s linear
position: absolute
top: 0.15em
left: 6px
display: block
margin-top: 3px
width: 5px
height: 9px
border-right: 3px solid $main-color
border-bottom: 3px solid $main-color
content: ''
opacity: 0
transform: rotate(45deg)
input[type=radio]:checked + .radio:before,
input[type=checkbox]:checked + .checkbox:before
border-color: $main-color
input[type=radio]:checked + .radio:after,
input[type=checkbox]:checked + .checkbox:after
opacity: 1
input[type=radio]:disabled + .radio:before,
input[type=checkbox]:disabled + .checkbox:before,
input[type=radio]:disabled + .radio:after,
input[type=checkbox]:disabled + .checkbox:after
border-color: $input-disabled-text-color
input[type=radio]:disabled + .radio,
input[type=checkbox]:disabled + .checkbox
border-color: $input-disabled-text-color
input[type=radio]:focus + .radio:before,
input[type=checkbox]:focus + .checkbox:before
border-color: $main-color
/*
* Date and time inputs
*/
input[type=date],
input[type=time]
vertical-align: top
font-family: 'Droid Sans', sans-serif
font-size: 100%
padding: 0.2em 0.3em
box-sizing: border-box
border: 2px solid $input-enabled-border-color
background: $input-enabled-background-color
color: $input-enabled-text-color
box-shadow: none /* :-moz-submit-invalid on FF */
transition: border-color 0.1s linear, background-color 0.1s linear
&:disabled
border: 2px solid $input-disabled-border-color
background: $input-disabled-background-color
color: $input-disabled-text-color
&:focus
border-color: $main-color
&[readonly]
border: 2px solid $input-disabled-border-color
background: $input-disabled-background-color
color: $input-disabled-text-color
.darktheme
input[type=date],
input[type=time]
border: 2px solid darken($input-enabled-border-color, 75%)
background: darken($input-enabled-background-color, 75%)
color: $input-enabled-text-color-darktheme
&:disabled
background: darken($input-disabled-background-color, 75%)
&[readonly]
background: darken($input-disabled-background-color, 75%)
/*
* Regular inputs
*/
select,
textarea,
input[type=text],
input[type=email],
input[type=password],
input[type=number]
vertical-align: top
font-family: 'Droid Sans', sans-serif
font-size: 100%
padding: 0.2em 0.3em
text-overflow: ellipsis
width: 100%
box-sizing: border-box
border: 2px solid $input-enabled-border-color
background: $input-enabled-background-color
color: $input-enabled-text-color
box-shadow: none /* :-moz-submit-invalid on FF */
transition: border-color 0.1s linear, background-color 0.1s linear
&:disabled
border: 2px solid $input-disabled-border-color
background: $input-disabled-background-color
color: $input-disabled-text-color
&:focus
border-color: $main-color
&[readonly]
border: 2px solid $input-disabled-border-color
background: $input-disabled-background-color
color: $input-disabled-text-color
.darktheme
select,
textarea,
input[type=text],
input[type=email],
input[type=password],
input[type=number]
border: 2px solid darken($input-enabled-border-color, 75%)
background: darken($input-enabled-background-color, 75%)
color: $input-enabled-text-color-darktheme
&:disabled
background: darken($input-disabled-background-color, 75%)
&[readonly]
background: darken($input-disabled-background-color, 75%)
input[readonly],
input[readonly]+.radio,
input[readonly]+.checkbox,
input:disabled+.radio,
input:disabled+.checkbox,
input:disabled
cursor: not-allowed
label.color
white-space: nowrap
position: relative
display: flex
input[type=text]
margin-right: 0.25em
width: auto
.preview
display: inline-block
text-align: center
padding: 0 0.5em
border: 2px solid black
&:after
content: 'A'
.background-preview
border-right: 0
color: transparent
.text-preview
border-left: 0
form.show-validation .input
input:invalid
outline: 0
border: 2px solid $input-bad-border-color
background: $input-bad-background-color
input:valid
outline: 0
border: 2px solid $input-good-border-color
background: $input-good-background-color
.darktheme form.show-validation .input
input:valid
background: darken($input-good-background-color, 75%)
/*
* Buttons
*/
button,
input[type=button],
input[type=submit]
cursor: pointer
font-size: 100%
padding: 0.2em 0.7em
border-radius: 0
border: 2px solid $button-enabled-background-color
background: $button-enabled-background-color
color: $button-enabled-text-color
outline: 0 /* something on Chrome */
-moz-appearance: none
-webkit-appearance: none
&:disabled
cursor: default
border-color: $button-disabled-background-color
background-color: $button-disabled-background-color
color: $button-disabled-text-color
&.discourage
border-color: transparent
background-color: transparent
color: $button-disabled-text-color
&:focus
border: 2px solid $text-color
select:-moz-focusring
text-shadow: 0
button::-moz-focus-inner,
input::-moz-focus-inner
border: 0
/*
* File dropper
*/
.file-dropper-holder
.file-dropper
display: block
background: $window-color
border: 3px dashed #eee
padding: 0.3em 0.5em
line-height: 140%
text-align: center
cursor: pointer
overflow: hidden
word-wrap: break-word
.url-holder
display: flex
margin-top: 0.5em
input, button
min-width: 0 /* firefox being sassy */
width: auto !important /* don't inherit anything weird */
input
flex: 1
button
margin-left: 0.5em
.darktheme .file-dropper-holder
.file-dropper
background: $window-color-darktheme
input[type=file]:disabled+.file-dropper
cursor: default
opacity: .5
input[type=file]:active+.file-dropper,
input[type=file]:focus+.file-dropper,
.file-dropper.active
border-color: $main-color
.autocomplete
position: absolute
z-index: 10
background: $window-color
border: 2px solid $main-color
display: none
font-size: 0.95em
ul
list-style-type: none
margin: 0
padding: 0
li
margin: 0
a
display: block
padding: 0.1em 0.5em
&.active a, a:hover
background: $button-enabled-background-color
color: $button-enabled-text-color
span
color: $button-enabled-text-color
.disabled
color: $inactive-link-color
.darktheme .autocomplete
background: $window-color-darktheme
ul li .disabled
color: $inactive-link-color-darktheme
.anticomplete
display: none

View file

@ -0,0 +1,327 @@
@import colors
@import mixins
$active-tab-text-color = $text-color
$active-tab-text-color-darktheme = $text-color-darktheme
$inactive-tab-text-color = $inactive-link-color
$inactive-tab-text-color-darktheme = $inactive-link-color-darktheme
/* latin */
@font-face
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(../fonts/open_sans.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
/* make <body> cover entire viewport */
html
height: 100%
body
min-height: 100%
body
background: $window-color
overflow-y: scroll
margin: 0
color: $text-color
font-family: 'Open Sans', sans-serif
font-size: 1em
line-height: 1.4
@media (max-width: 800px)
font-size: 0.875em
@media (max-width: 1200px)
font-size: 0.95em
body.darktheme
color: $text-color-darktheme
background: $window-color-darktheme
h1, h2, h3
font-weight: normal
margin-bottom: 1em
h1
font-size: 2em
h2
font-size: 1.5em
p,
ol,
ul
margin: 1em 0
th
font-weight: normal
a
cursor: pointer
color: $main-color
text-decoration: none
transition: color 0.1s linear
&.inactive
color: $inactive-link-color
cursor: default
&.icon
color: $inactive-link-color
opacity: .5
&:focus
outline: 2px solid $main-color
.vim-nav-hint
position: absolute
visibility: hidden
.darktheme a
&.inactive
color: $inactive-link-color-darktheme
&.icon
color: $inactive-link-color-darktheme
a.append, span.append
margin-left: 1em
form .fa-question-circle-o
font-size: 110%
vertical-align: middle
#content-holder
padding: 1.5em
text-align: center
@media (max-width: 1000px)
padding: 1em
>.content-wrapper
box-sizing: border-box /* make max-width: 100% on this element include padding */
text-align: left
display: inline-block
margin: 0 auto
>*:first-child, form h1
margin-top: 0
nav.buttons
ul
display: block
max-width: 100%
white-space: nowrap
overflow-x: auto
&::-webkit-scrollbar
height: 6px
background-color: $scrollbar-bg-color
&::-webkit-scrollbar-thumb
background-color: $scrollbar-thumb-color
>.content-wrapper:not(.transparent)
background: $top-navigation-color
padding: 1.8em
@media (max-width: 1000px)
padding: 1.5em
.content,
.content .subcontent
>*:last-child
margin-bottom: 0
.darktheme #content-holder
>.content-wrapper:not(.transparent)
background: $top-navigation-color-darktheme
hr
border: 0
border-top: 1px solid $line-color
margin: 1em 0
padding: 0
.darktheme hr
border-top: 1px solid darken($line-color, 25%)
nav
ul
list-style-type: none
padding: 0
margin: 0
display: inline-block
li
display: block
padding: 0
margin: 0
a
display: inline-block
img
margin: 0
vertical-align: top /* fix ghost margin under the image */
&.buttons
margin: 1em 0
line-height: 2.3em
vertical-align: middle
ul
li
display: inline-block
li a
padding: 0 1.2em
li:not(.active) a
color: $inactive-tab-text-color
li:hover:not(.active) a
color: $active-tab-text-color
li.active a
background: $active-tab-background-color
color: $active-tab-text-color
:focus
background: $focused-tab-background-color
outline: 0
&#top-navigation
background: $top-navigation-color
margin: 0
ul
display: block
text-align: right
li
display: inline-block
float: left
a
padding: 0 1.5em
#mobile-navigation-toggle
display: none
width: 100%
padding: 0 1em
line-height: 2.3em
font-family: inherit
border: none
background: none
color: $active-tab-text-color
.site-name
display: block
float: left
max-width: 50vw
overflow: hidden
text-overflow: ellipsis
.toggle-icon
display: block
float: right
@media (max-width: 1000px)
text-align: left
li
display: none
float: none
a
display: block
padding: 0 1em
#mobile-navigation-toggle
display: block
&.opened
li
display: block
ul li[data-name=account],
ul li[data-name=register],
ul li[data-name=login],
ul li[data-name=logout],
ul li[data-name=settings],
ul li[data-name=help]
float: none
.access-key
text-decoration: underline
.thumbnail
width: 1.5em
height: 1.5em
margin: calc((2.3em - 1.5em) / 2)
margin-right: 0.6em
margin-left: calc(0.6em - 1.2em)
float: left
@media (max-width: 1000px)
display: none
.darktheme nav
&.buttons
ul
li:not(.active) a
color: $inactive-tab-text-color-darktheme
li:hover:not(.active) a
color: $active-tab-text-color-darktheme
li.active a
background: $active-tab-background-color-darktheme
color: $active-tab-text-color-darktheme
:focus
background: $focused-tab-background-color-darktheme
&#top-navigation
background: $top-navigation-color-darktheme
ul
#mobile-navigation-toggle
color: $text-color-darktheme
a .access-key
text-decoration: underline
.messages
margin: 0 auto
text-align: left
.message
box-sizing: border-box
width: 100%
max-width: 40em
margin: 0 0 1em 0
display: inline-block
text-align: left
padding: 0.5em 1em
&.info
border: 1px solid $message-info-border-color
background: $message-info-background-color
&.error
border: 1px solid $message-error-border-color
background: $message-error-background-color
&.success
border: 1px solid $message-success-border-color
background: $message-success-background-color
.darktheme .messages
.message
&.info
border: 1px solid darken($message-info-border-color, 30%)
background: darken($message-info-background-color, 60%)
&.error
border: 1px solid darken($message-error-border-color, 30%)
background: darken($message-error-background-color, 60%)
&.success
border: 1px solid darken($message-success-border-color, 30%)
background: darken($message-success-background-color, 80%)
.thumbnail
/*background-image: attr(data-src url)*/ /* not available yet */
vertical-align: middle
background-repeat: no-repeat
background-size: cover
background-position: center
display: inline-block
width: 20px
height: 20px
&.empty
background-image:
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
background-repeat: repeat
background-size: 20px 20px
img
opacity: 0
width: 100%
height: 100%
video
width: 100%
height: 100%
.flexbox-dummy
height: 0 !important
padding-top: 0 !important
padding-bottom: 0 !important
margin-top: 0 !important
margin-bottom: 0 !important
.table-wrap
overflow-x: auto
&::-webkit-scrollbar
height: 6px
background-color: $scrollbar-bg-color
&::-webkit-scrollbar-thumb
background-color: $scrollbar-thumb-color
/* hack to prevent text from being copied */
[data-pseudo-content]:before {
content: attr(data-pseudo-content)
}

View file

@ -0,0 +1,32 @@
@import colors
.expander
&.collapsed
margin-bottom: 1em
&>*
display: none
&>header
display: block
header
background: $active-tab-background-color
line-height: 2em
a
padding: 0 0.5em
display: block
color: mix($text-color, $inactive-link-color)
font-size: 120%
i
font-size: 1em
color: $inactive-link-color
float: right
line-height: 2em
.expander-content
padding: 0.5em 0.5em 2em 0.5em
.darktheme .expander
header
background: $active-tab-background-color-darktheme
a
color: mix($text-color-darktheme, $inactive-link-color-darktheme)
i
color: $inactive-link-color-darktheme

38
client/css/help-view.styl Normal file
View file

@ -0,0 +1,38 @@
@import colors
#help
width: 100%
max-width: 45em
nav
margin-bottom: 1.5em
td, th
padding: 0 0.5em
&:first-child
white-space: pre
.section
margin-top: 2em
h1
margin-top: 2.5em
font-size: 1.6em
&:first-child
margin-top: 0
@media (max-width: 1000px)
margin-top: 1.5em
&:first-child
margin-top: 0
nav
ul
margin: 0 auto
text-align: left
nav.secondary
font-size: 0.95em
@media (max-width: 600px)
th, thead
display: none
table, tr, td, tbody
display: block
tr
margin-bottom: 0.8em
pre
white-space: pre-wrap

67
client/css/home-view.styl Normal file
View file

@ -0,0 +1,67 @@
#home
text-align: center !important
max-width: 100%
header
margin-bottom: 1em
h1
line-height: initial
font-size: 2.5em
margin: 0
.messages
text-align: center
.message
margin: 0 auto 2em auto
form
display: inline-block
width: auto
vertical-align: middle
margin: 0 0 2em 0
text-align: left
white-space: nowrap
input
width: auto
.sep
margin: 0 0.75em
@media (max-width: 500px)
.sep, a
display: none
.post-container
margin-bottom: 2em
display: flex
align-items: center
justify-content: center
&:empty
margin-bottom: 0
nav
a
padding: 0.5em
aside
margin-bottom: 0.3em
font-size: 90%
white-space: nowrap
footer
line-height: 150%
font-size: 80%
ul
padding: 0
text-align: center
li
display: inline
white-space: nowrap
@media (max-width: 800px)
display: block
.sep
word-spacing: 1.1em
background-repeat: no-repeat
background-position: 50% 50%
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12'><circle cx='6' cy='6' r='2' fill='%23000000'/></svg>")
.thumbnail
margin-right: 0.4em

6
client/css/mixins.styl Normal file
View file

@ -0,0 +1,6 @@
unselectable()
-webkit-user-select: none
-moz-user-select: none
-ms-user-select: none
-o-user-select: none
user-select: none

23
client/css/nprogress.styl Normal file
View file

@ -0,0 +1,23 @@
@import colors
#nprogress
pointer-events: none
.bar
background: $main-color
position: fixed
z-index: 1031
top: 0
left: 0
width: 100%
height: 2px
.spinner-icon
display: none
.peg
display: none
.nprogress-custom-parent
overflow: hidden
position: relative

35
client/css/pager.styl Normal file
View file

@ -0,0 +1,35 @@
@import colors
.pager
nav
.disabled
opacity: .5
.page
position: relative
.page-header
margin: 0.5em 0
position: relative
&:before
display: block
content: ''
position: absolute
left: 0
top: 50%
right: 0
height: 3px
background: $top-navigation-color
z-index: 1
span
position: relative
background: $window-color
padding: 0 1em
z-index: 2
.darktheme .pager
.page
.page-header
&:before
background: $top-navigation-color-darktheme
span
background: $window-color-darktheme

View file

@ -0,0 +1,2 @@
#password-reset
max-width: 30em

View file

@ -0,0 +1,29 @@
@import colors
.content-wrapper.pool-categories
width: 100%
max-width: 45em
table
border-spacing: 0
width: 100%
tr.default td
background: $default-pool-category-background-color
td, th
padding: .4em
&.color
input[type=text]
width: 8em
&.usages
text-align: center
&.remove, &.set-default
white-space: pre
th
white-space: nowrap
&:first-child
padding-left: 0
&:last-child
padding-right: 0
tfoot
display: none
form
width: auto

View file

@ -0,0 +1,58 @@
@import colors
div.pool-input
position: relative
.main-control
display: flex
input
flex: 5
button
flex: 1
margin: 0 0 0 0.5em
ul.compact-pools
width: 100%
margin: 0.5em 0 0 0
padding: 0
li
margin: 0
width: 100%
line-height: 140%
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
transition: background-color 0.5s linear
a
display: inline
a:focus
outline: 0
box-shadow: inset 0 0 0 2px $main-color
&.implication
background: $implied-pool-background-color
color: $implied-pool-text-color
&.new
background: $new-pool-background-color
color: $new-pool-text-color
&.duplicate
background: $duplicate-pool-background-color
color: $duplicate-pool-text-color
i
padding-right: 0.4em
div.pool-input, ul.compact-pools
.pool-usages, .pool-weight, .remove-pool
color: $inactive-link-color
unselectable()
.pool-usages, .pool-weight
font-size: 90%
.pool-usages, .pool-weight
margin-left: 0.7em
.remove-pool
margin-right: 0.5em
.darktheme
div.pool-input, ul.compact-pools
.pool-usages, .pool-weight, .remove-pool
color: $inactive-link-color-darktheme

View file

@ -0,0 +1,63 @@
@import colors
.pool-list
table
width: 100%
border-spacing: 0
text-align: left
line-height: 1.3em
tr:hover td
background: $top-navigation-color
th, td
padding: 0.1em 0.5em
th
white-space: nowrap
background: $top-navigation-color
.names
width: 84%
.post-count
text-align: center
width: 8%
.creation-time
text-align: center
width: 8%
white-space: pre
ul
list-style-type: none
margin: 0
padding: 0
display: inline
li
padding: 0
display: inline
&:not(:last-child):after
content: ', '
@media (max-width: 800px)
.posts
display: none
.darktheme .pool-list
table
tr:hover td
background: $top-navigation-color-darktheme
th
background: $top-navigation-color-darktheme
.pool-list-header
label
display: none !important
text-align: left
form
width: auto
input[name=search-text]
width: 25em
@media (max-width: 1000px)
width: 100%
.append
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.darktheme .pool-list-header
.append
color: $inactive-link-color-darktheme

33
client/css/pool-view.styl Normal file
View file

@ -0,0 +1,33 @@
#pool
width: 100%
max-width: 40em
h1
word-break: break-all
line-height: 130%
margin-top: 0
form
width: 100%
.pool-edit
textarea
height: 10em
.pool-summary
section
&.description
margin: 1.5em 0 0 0
&.details
vertical-align: top
padding-right: 0.5em
ul
margin: 0
padding: 0
list-style-type: none
li
display: inline
margin: 0
padding: 0
li:not(:last-of-type):after
content: ', '
ul:empty:after
content: '(none)'
section
margin-bottom: 1em

View file

@ -0,0 +1,29 @@
@import colors
.post-container
.post-content.transparency-grid img
background-image:
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-size: 20px 20px
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
text-align: center
.post-content
text-align: left
margin: 0 auto
position: relative
.resize-listener
position: absolute
left: 0
right: 0
top: 0
bottom: 0
width: 100%
height: 100%
img
image-orientation: from-image

View file

@ -0,0 +1,37 @@
#post
width: 100%
max-width: 40em
h1
margin-top: 0
form
width: 100%
.buttons i
margin-right: 0.5em
.post-merge
.left-post-container
width: 47%
float: left
.right-post-container
width: 47%
float: right
.post-mirror
margin-bottom: 1em
&:after
display: block
height: 1px
content: ' '
clear: both
.post-thumbnail .thumbnail
width: 100%
height: 9em
.target-post .thumbnail
margin-right: 0.35em
.target-post, .target-post-content
margin: 1em 0
header
margin-bottom: 1em
label
display: inline-block
margin-top: 2px
input[type=text]
width: 6em

View file

@ -0,0 +1,273 @@
@import colors
.post-list
ul
list-style-type: none
margin: 0
padding: 0
display: flex
align-content: flex-end
flex-wrap: wrap
margin: 0 -0.25em
li
position: relative
flex-grow: 1
margin: 0 0.25em 0.5em 0.25em
display: inline-block
text-align: left
min-width: 10em
width: 12vw
&:not(.flexbox-dummy)
min-height: 7.5em
height: 9vw
.thumbnail-wrapper
display: inline-block
width: 100%
height: 100%
line-height: 80%
font-size: 80%
color: white
outline-offset: -3px
box-shadow: 0 0 0 1px rgba(0,0,0,0.2)
.type, .stats
position: absolute
bottom: 0.5em
padding: 0.33em 0.5em
background: rgba(0,0,0,0.5)
height: 1em
.type
float: left
left: 0.5em
&[data-type=image]
display: none
.stats
float: right
right: 0.5em
text-align: right
i
margin-right: 0.25em
.icon:not(:first-of-type)
margin-left: 1em
.edit-overlay
position: absolute
top: 0.5em
left: 0.5em
.tag-flipper
display: inline-block
padding: 0.5em
box-sizing: border-box
border: 0
&:after
display: inline-block
width: 1em
height: 1em
text-align: center
line-height: 1em
font-size: 2.2em
&.tagged
background: rgba(0, 230, 0, 0.7)
&:after
color: white
content: '-'
&:not(.tagged)
background: rgba(255, 0, 0, 0.7)
&:after
color: white
content: '+'
&[data-disabled]
background: rgba(200, 200, 200, 0.7)
.safety-flipper a
display: inline-block
margin: 0.1em
box-sizing: border-box
border: 0
display: inline-block
width: 1.2em
height: 1.2em
text-align: center
line-height: 1em
font-size: 1.6em
border: 3px solid
&.safety-safe
background-color: darken($safety-safe, 5%)
border-color: @background-color
&:not(.active)
background-color: alpha(@background-color, 0.3)
&.safety-sketchy
background-color: $safety-sketchy
border-color: @background-color
&:not(.active)
background-color: alpha(@background-color, 0.3)
&.safety-unsafe
background-color: $safety-unsafe
border-color: @background-color
&:not(.active)
background-color: alpha(@background-color, 0.3)
&[data-disabled]
background: rgba(200, 200, 200, 0.7)
.delete-flipper
display: inline-block
padding: 0.5em
box-sizing: border-box
border: 0
&:after
display: inline-block
width: 1em
height: 1em
text-align: center
line-height: 1em
font-size: 2.2em
&.delete
background: rgba(255, 0, 0, 0.7)
&:after
color: white
font-family: FontAwesome;
content: "\f1f8"; // fa-trash
&:not(.delete)
background: rgba(200, 200, 200, 0.7)
&:after
color: white
content: '-'
.thumbnail
width: 100%
height: 100%
outline-offset: -3px
&:not(.empty)
background-position: 50% 30%
.thumbnail-wrapper.no-tags
.thumbnail
outline: 4px solid $post-thumbnail-no-tags-border-color
&:hover
background: $post-thumbnail-border-color
.thumbnail
opacity: .9
&:hover a, a:active, a:focus
.thumbnail
outline: 4px solid $main-color !important
.post-flow
ul
li
min-width: inherit
width: inherit
&:not(.flexbox-dummy)
height: 14vw
.thumbnail
outline-offset: -1px
.thumbnail-wrapper.no-tags
.thumbnail
outline: 2px solid $post-thumbnail-no-tags-border-color
&:hover a, a:active, a:focus
.thumbnail
outline: 2px solid $main-color !important
.post-list-header
white-space: nowrap
text-align: left
label
display: none !important
form
width: auto
margin-bottom: 0.75em
*
vertical-align: top
@media (max-width: 1000px)
display: block
&.bulk-edit-tags:not(.opened), &.bulk-edit-safety:not(.opened)
float: left
margin-right: 1em
input
margin-bottom: 0.25em
margin-right: 0.25em
input[name=search-text]
width: 25em
@media (max-width: 1000px)
display: block
width: 100%
margin-bottom: 0.5em
.append
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.bulk-edit
&:not(.opened)
.close
display: none
&.opened
.open
display: none
&.hidden
display: none
.bulk-edit-tags
&.opened
.hint
@media (max-width: 1000px)
display: block
margin-bottom: 0.5em
&:not(.opened)
[type=text],
.start
display: none
.hint
display: none
input[name=tag]
width: 24em
@media (max-width: 1000px)
display: block
width: 100%
margin-bottom: 0.5em
.append
&.open,
&.hint
@media (max-width: 1000px)
margin-left: 0
.hint
margin-right: 1em
.bulk-edit-safety
.append
@media (max-width: 1000px)
margin-left: 0
.bulk-edit-delete
&.opened
.start
@media (max-width: 1000px)
margin-left: 0
&:not(.opened)
.start
display: none
.append.open
@media (max-width: 1000px)
margin-left: 0
.start
margin-left: 1em
.safety
margin-right: 0.25em
&.safety-safe
background-color: $safety-safe
border-color: @background-color
&.disabled
background-color: alpha(@background-color, 0.15)
&.safety-sketchy
background-color: $safety-sketchy
border-color: @background-color
&.disabled
background-color: alpha(@background-color, 0.15)
&.safety-unsafe
background-color: $safety-unsafe
border-color: @background-color
&.disabled
background-color: alpha(@background-color, 0.15)

View file

@ -0,0 +1,178 @@
@import colors
.post-view
width: 100%
display: flex !important
flex-direction: row
>.sidebar
margin-right: 1em
min-width: 21em
max-width: 21em
line-height: 160%
a:active
border: 0
outline: 0
>.sidebar>nav.buttons, >.content nav.buttons
margin-top: 0
display: flex
flex-wrap: wrap
article
flex: 1 0 33%
a
display: inline-block
width: 100%
padding: 0.3em 0
text-align: center
vertical-align: middle
transition: background 0.2s linear, box-shadow 0.2s linear
&:not(.inactive):hover
background: lighten($main-color, 90%)
i
font-size: 140%
text-align: center
@media (max-width: 800px)
margin-top: 0.6em
margin-bottom: 0.6em
>.content
width: 100%
.post-container
margin-bottom: 0.6em
.post-content
margin: 0
.after-mobile-controls
width: 100%
.darktheme .post-view
>.sidebar, >.content
nav.buttons
article
a:not(.inactive):hover
background: unset
box-shadow: inset 0 0 0 0.3em $main-color
@media (max-width: 800px)
.post-view
flex-wrap: wrap
>.after-mobile-controls
order: 3
>.sidebar
order: 2
min-width: 100%
max-width: 0
margin-right: 0
>.content
order: 1
.post-view .readonly-sidebar
.details
i
margin-right: 0.6em
display: inline-block
width: 1em
text-align: center
.safety-safe
color: $safety-safe
.safety-sketchy
color: $safety-sketchy
.safety-unsafe
color: $safety-unsafe
.upload-info
.thumbnail
width: 1em
height: 1em
margin: -0.1em 0.6em 0 0
.zoom
margin-top: 1em
a
display: inline-block
.active
text-decoration: underline
.social
margin-top: 1em
.score-container
float: left
margin-right: 3em
.downvote i
text-align: right
i
text-align: left
margin: 0
.value
text-align: center
display: inline-block
width: 2em
.relations
margin-top: 2em
h1
margin-bottom: 0.5em
.thumbnail
width: 4em
height: 3em
li
margin: 0 0.3em 0.3em 0
display: inline-block
.tags
margin-top: 2em
h1
margin-bottom: 0.5em
.post-view .edit-sidebar
.expander-content
section:not(:last-child)
margin-bottom: 1em
.safety
&>label
width: 100%
.radio-wrapper
display: flex
flex-wrap: wrap
.radio-wrapper label
flex-grow: 1
display: inline-block
.management
ul
list-style-type: none
margin: 0
padding: 0
li
margin: 0
padding: 0
.post-source
textarea
white-space: pre
overflow-wrap: normal
overflow-x: scroll
form
width: auto
label:not(.file-dropper)
margin-bottom: 0.3em
display: block
input[type=submit],
input[type=button],
button
width: 100%
&:focus
border: 2px solid $text-color !important
.messages
margin-top: 1em

View file

@ -0,0 +1,69 @@
@import colors
.post-overlay
&[data-state=ready-to-draw],
&[data-state=drawing-rectangle],
&[data-state=drawing-polygon]
&:after
box-sizing: border-box
border: 0.3em dashed $active-note-overlay-border-color
background: $active-note-overlay-background-color
display: block
content: ' '
pointer-events: none
position: absolute
width: 100%
height: 100%
left: 0
right: 0
top: 0
bottom: 0
.notes-overlay
g
stroke-width: 1px
polygon
fill: $note-background-color
stroke: $note-border-color
pointer-events: auto
ellipse
display: none
g[data-state=editing], g[data-state=drawing]
stroke-width: 2px
polygon
fill: $edited-note-background-color
stroke: $edited-note-border-color
ellipse
fill: $edited-note-border-color
display: block
&.nearby
fill: $hovered-note-point-color
g[data-state=drawing]
ellipse:first-of-type
fill: $first-note-point-color
&.nearby
fill: $hovered-first-note-point-color
.note-text
position: absolute
max-width: 22.5em
display: none
&:not([data-state=read-only])
pointer-events: none
&>.wrapper
background: $note-text-background-color
padding: 0.3em 0.6em
border: 1px solid $note-text-border-color
color: $note-text-text-color
box-sizing: border-box
p:last-of-type
margin-bottom: 0
p:first-of-type
margin-top: 0

182
client/css/post-upload.styl Normal file
View file

@ -0,0 +1,182 @@
@import colors
$upload-header-background-color = $top-navigation-color
$upload-header-background-color-darktheme = $top-navigation-color-darktheme
$upload-border-color = #DDD
$cancel-button-color = tomato
#post-upload
form
width: 100%
max-width: 40em
margin: 0 auto
text-align: left
&.inactive input[type=submit],
&.inactive .skip-duplicates
&.inactive .always-upload-similar
&.inactive .pause-remain-on-error
&.uploading input[type=submit],
&.uploading .skip-duplicates,
&.uploading .always-upload-similar
&.uploading .pause-remain-on-error
&:not(.uploading) .cancel
display: none
.dropper-container
margin: 0 auto
.file-dropper
font-size: 150%
padding: 2em
small
font-size: 60%
input[type=submit]
margin-top: 1em
.cancel
margin-top: 1em
background: $cancel-button-color
border-color: $cancel-button-color
&:focus
border: 2px solid $text-color
.skip-duplicates
margin-left: 1em
.always-upload-similar
margin-left: 1em
.pause-remain-on-error
margin-left: 1em
form>.messages
margin-top: 1em
.uploadables-container
list-style-type: none
margin: 0
padding: 0
.uploadable-container
clear: both
margin: 0 0 1.2em 0
padding-left: 13em
img
width: 100%
height: 100%
video
width: 100%
height: 100%
&>.thumbnail-wrapper
float: left
width: 12em
height: 8em
margin: 0 0 0 -13em
.thumbnail
width: 100%
height: 100%
.uploadable
border: 1px solid $upload-border-color
min-height: 8em
box-sizing: border-box
header
line-height: 1.5em
padding: 0.25em 1em
text-align: left
background: $upload-header-background-color
border-bottom: 1px solid $upload-border-color
nav
&:first-of-type
float: left
a
margin: 0 0.5em 0 0
&:last-of-type
float: right
a
margin: 0 0 0 0.5em
ul
list-style-type: none
ul, li
display: inline-block
margin: 0
padding: 0
span.filename
padding: 0 0.5em
display: block
overflow: hidden
white-space: nowrap
text-overflow: ellipsis
.body
margin: 1em
.anonymous
margin: 0.3em 0
.safety
margin: 0.3em 0
label
display: inline-block
margin-right: 1em
.options div
display: inline-block
margin: 0 1em 0 0
.messages
margin-top: 1em
.message:last-child
margin-bottom: 0
.lookalikes
list-style-type: none
margin: 0
padding: 0
li
clear: both
margin: 1em 0 0 0
padding-left: 7em
font-size: 90%
.thumbnail-wrapper
float: left
width: 6em
height: 4em
margin: 0 0 0 -7em
.thumbnail
width: 100%
height: 100%
.description
margin-right: 0.5em
display: inline-block
.controls
float: right
display: inline-block
&:first-child .move-up
color: $inactive-link-color
&:last-child .move-down
color: $inactive-link-color
.darktheme &:first-child .move-up
color: $inactive-link-color-darktheme
.darktheme &:last-child .move-down
color: $inactive-link-color-darktheme
.darktheme #post-upload .uploadables-container .uploadable-container
.uploadable header
background: $upload-header-background-color-darktheme
&:first-child .move-up
color: $inactive-link-color-darktheme
&:last-child .move-down
color: $inactive-link-color-darktheme

View file

@ -0,0 +1,64 @@
$snapshot-created-background-color = #E0F5E0
$snapshot-modified-background-color = #E0F5FF
$snapshot-deleted-background-color = #FDE5E5
$snapshot-merged-background-color = #FEC
.snapshot-list
text-align: left
ul
margin: 0 auto
padding: 0
width: 100%
max-width: 35em
list-style-type: none
li
margin-bottom: 1em
&:last-child
margin-bottom: 0
.time
float: right
div
padding: 0.1em 0.5em
.thumbnail
margin: 0 0.4em 0 0
&:empty
padding: 0
div.operation-created
background: $snapshot-created-background-color
&+.details
background: alpha(@background, 50%)
div.operation-modified
background: $snapshot-modified-background-color
&+.details
background: alpha(@background, 50%)
div.operation-deleted
background: $snapshot-deleted-background-color
&+.details
background: alpha(@background, 50%)
div.operation-merged
background: $snapshot-merged-background-color
&+.details
background: alpha(@background, 50%)
.darktheme .snapshot-list ul li
div.operation-created
background: darken($snapshot-created-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-modified
background: darken($snapshot-modified-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-deleted
background: darken($snapshot-deleted-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-merged
background: darken($snapshot-merged-background-color, 80%)
&+.details
background: alpha(@background, 50%)

View file

@ -0,0 +1,29 @@
@import colors
.content-wrapper.tag-categories
width: 100%
max-width: 45em
table
border-spacing: 0
width: 100%
tr.default td
background: $default-tag-category-background-color
td, th
padding: .4em
&.color
input[type=text]
width: 8em
&.usages
text-align: center
&.remove, &.set-default
white-space: pre
th
white-space: nowrap
&:first-child
padding-left: 0
&:last-child
padding-right: 0
tfoot
display: none
form
width: auto

View file

@ -0,0 +1,161 @@
@import colors
div.tag-input
position: relative
.main-control
display: flex
input
flex: 5
button
flex: 1
margin: 0 0 0 0.5em
.tag-suggestions
position: absolute
z-index: 5
top: 0
left: 100%
&:not(.shown)
display: none
&.translucent
opacity: .5
&:before
margin-left: 0.5em
margin-top: 0.5em
position: absolute
display: block
background: $tag-suggestions-header-color
border-left: 1px solid $tag-suggestions-border-color
border-bottom: 1px solid $tag-suggestions-border-color
width: 0.707107em
height: 0.707107em
content: ' '
transform: rotate(45deg)
transform-origin: 0 0%
.buttons
float: right
a
margin-left: 1em
color: $inactive-link-color
.wrapper
margin-left: 0.5em
background: $window-color
border: 1px solid $tag-suggestions-border-color
width: 15em
word-break: break-all
p
background: $tag-suggestions-header-color
padding: 0.2em 1em
margin: 0
ul
list-style-type: none
margin: 0
overflow-y: auto
overflow-x: none
max-height: 20em
padding: 0.5em 1em 0 1em
li:last-child
border-bottom: 0.5em solid alpha($window-color, 0)
li
margin: 0
font-size: 90%
line-height: 1.3
a, span
display: inline-block
vertical-align: bottom
.add-tag
white-space: nowrap
overflow: hidden
max-width: 10em
text-overflow: ellipsis
.tag-weight
margin: 0 1em 0 0
p
margin: 0
.append
color: $inactive-link-color
margin-left: 0.7em
font-size: 90%
unselectable()
@keyframes tag-added-to-post
from
max-height: 0
to
max-height: 5em
ul.compact-tags
width: 100%
margin: 0.5em 0 0 0
padding: 0
li
margin: 0
width: 100%
line-height: 140%
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
transition: background-color 0.5s linear
a
display: inline
a:focus
outline: 0
box-shadow: inset 0 0 0 2px $main-color
// these 3 added when tag is added to ul
&.added, &.new, &.implication
animation: tag-added-to-post 1s ease forwards
&.implication
color: $implied-tag-text-color
background-color: $implied-tag-background-color
&.new
color: $new-tag-text-color
background-color: $new-tag-background-color
&.duplicate
color: $duplicate-tag-text-color
background-color: $duplicate-tag-background-color
i
padding-right: 0.4em
.darktheme ul.compact-tags
li
&.new
background-color: darken($new-tag-background-color, 80%)
&.implication
background-color: darken($implied-tag-background-color, 85%)
&.duplicate
background-color: darken($duplicate-tag-background-color, 80%)
div.tag-input, ul.compact-tags
.tag-usages, .tag-weight, .remove-tag
color: $inactive-link-color
unselectable()
.tag-usages, .tag-weight
font-size: 90%
.tag-usages, .tag-weight
margin-left: 0.7em
.remove-tag
margin-right: 0.5em
.darktheme
div.tag-input .tag-suggestions
.buttons a
color: $inactive-link-color-darktheme
.wrapper
background: $window-color-darktheme
ul li:last-child
border-bottom: 0.5em solid alpha($window-color-darktheme, 0)
p
background: darken($tag-suggestions-header-color, 80%)
.append
color: $inactive-link-color-darktheme
div.tag-input, ul.compact-tags
.tag-usages, .tag-weight, .remove-tag
color: $inactive-link-color-darktheme

View file

@ -0,0 +1,67 @@
@import colors
.tag-list
table
width: 100%
border-spacing: 0
text-align: left
line-height: 1.3em
tr:hover td
background: $top-navigation-color
th, td
padding: 0.1em 0.5em
th
white-space: nowrap
background: $top-navigation-color
.names
width: 28%
.implications
width: 28%
.suggestions
width: 28%
.usages
text-align: center
width: 8%
.creation-time
text-align: center
width: 8%
white-space: pre
ul
list-style-type: none
margin: 0
padding: 0
display: inline
li
padding: 0
display: inline
&:not(:last-child):after
content: ', '
@media (max-width: 800px)
.implications, .suggestions
display: none
.darktheme .tag-list
table
tr:hover td
background: $top-navigation-color-darktheme
th
background: $top-navigation-color-darktheme
.tag-list-header
label
display: none !important
text-align: left
form
width: auto
input[name=search-text]
width: 25em
@media (max-width: 1000px)
width: 100%
.append
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.darktheme .tag-list-header
.append
color: $inactive-link-color-darktheme

33
client/css/tag-view.styl Normal file
View file

@ -0,0 +1,33 @@
#tag
width: 100%
max-width: 40em
h1
word-break: break-all
line-height: 130%
margin-top: 0
form
width: 100%
.tag-edit
textarea
height: 10em
.tag-summary
section
&.description
margin: 1.5em 0 0 0
&.details
vertical-align: top
padding-right: 0.5em
ul
margin: 0
padding: 0
list-style-type: none
li
display: inline
margin: 0
padding: 0
li:not(:last-of-type):after
content: ', '
ul:empty:after
content: '(none)'
section
margin-bottom: 1em

View file

@ -0,0 +1,51 @@
@import colors
.user-list
ul
list-style-type: none
padding: 0
display: flex
align-content: flex-end
flex-wrap: wrap
margin: 0 -0.5em
li
flex-grow: 1
width: 20em
margin: 0 0.5em 1em 0.5em
padding: 0.75em
vertical-align: top
background: $top-navigation-color
text-align: left
.wrapper
display: flex
.details
font-size: 90%
line-height: 130%
.image
margin: 0.25em 0.6em 0.25em 0
.thumbnail
width: 3em
height: 3em
.darktheme .user-list
ul li
background: $top-navigation-color-darktheme
.user-list-header
label
display: none !important
text-align: left
form
width: auto
input[name=search-text]
width: 25em
@media (max-width: 1000px)
width: 100%
.append
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.darktheme .user-list-header
.append
color: $inactive-link-color-darktheme

View file

@ -0,0 +1,29 @@
@import colors
#user-registration
padding-bottom: calc(2vw - 1em) !important
form
float: left
margin-right: 3em
margin-bottom: 1em
.info
float: left
border-radius: 0.2em
width: 20em
margin-bottom: 1em
ul
line-height: 1.8em
list-style-type: none
margin: 0
padding: 0
li
margin: 0
padding: 0
i
margin-right: 0.5em
i.fa
color: $main-color
p:first-child
margin: 0 0 0.5em 0
p:last-child
margin-bottom: 0

82
client/css/user-view.styl Normal file
View file

@ -0,0 +1,82 @@
@import colors
$token-border-color = $active-tab-background-color
#user
width: 100%
max-width: 35em
nav.text-nav
margin-bottom: 1.5em
#user-summary
.thumbnail
width: 6em
height: 6em
margin: 0 1.5em 1.5em 0
float: left
.basic-info
list-style-type: none
margin: 0
div
clear: both
nav
float: left
width: 45%
margin-right: 1em
#user-edit
form
width: 100%
.avatar
#avatar-content
float: right
width: 65%
margin-top: .5em
#avatar-radio
float: left
width: 30%
&:after
content: ' '
display: block
height: 1px
clear: both
#user-tokens
.token-flex-container
width: 100%
display: flex;
flex-direction column;
padding-bottom: 0.5em;
.full-width
width: 100%
.token-flex-row
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0.2em;
.no-wrap
white-space: nowrap;
.token-input
min-height: 2em;
line-height: 2em;
text-align: center;
.token-flex-column
display: flex;
flex-direction: column;
.token-flex-labels
padding-right: 0.5em
hr
border-top: 3px solid $token-border-color
form
width: 100%;
#user-delete form
width: 100%

11
client/docker-start.sh Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/dumb-init /bin/sh
# Integrate environment variables
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
/etc/nginx/nginx.conf
sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \
/var/www/index.htm \
/var/www/manifest.json
# Start server
exec nginx

Binary file not shown.

85
client/html/comment.tpl Normal file
View file

@ -0,0 +1,85 @@
<div class='comment-container'>
<div class='avatar'>
<% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %>
<a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'>
<% } %>
<%= ctx.makeThumbnail(ctx.user ? ctx.user.avatarUrl : null) %>
<% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %>
</a>
<% } %>
</div>
<div class='comment'>
<header>
<nav class='edit tabs'>
<ul>
<li class='edit'><a href>Write</a></li>
<li class='preview'><a href>Preview</a></li>
</ul>
</nav>
<nav class='readonly'><%
%><strong><span class='nickname'><%
%><% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %><%
%><a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'><%
%><% } %><%
%><%- ctx.user ? ctx.user.name : 'Deleted user' %><%
%><% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %><%
%></a><%
%><% } %><%
%></span></strong>
<span class='date'><%
%>commented <%= ctx.makeRelativeTime(ctx.comment ? ctx.comment.creationTime : null) %><%
%></span><%
%><wbr><%
%><span class='score-container'></span><%
%><% if (ctx.canEditComment || ctx.canDeleteComment) { %><%
%><span class='action-container'><%
%><% if (ctx.canEditComment) { %><%
%><a href class='edit'><%
%><i class='fa fa-pencil'></i>&nbsp;edit<%
%></a><%
%><% } %><%
%><% if (ctx.canDeleteComment) { %><%
%><a href class='delete'><%
%><i class='fa fa-remove'></i>&nbsp;delete<%
%></a><%
%><% } %><%
%></span><%
%><% } %><%
%></nav><%
%></header>
<form class='body'>
<div class='keep-height'>
<div class='tab preview'>
<div class='comment-content'>
<%= ctx.makeMarkdown(ctx.comment ? ctx.comment.text : '') %>
</div>
</div>
<div class='tab edit'>
<textarea required minlength=1><%- ctx.comment ? ctx.comment.text : '' %></textarea>
</div>
</div>
<nav class='edit'>
<div class='messages'></div>
<input type='submit' class='save-changes' value='Save'/>
<% if (!ctx.onlyEditing) { %>
<input type='button' class='cancel-editing discourage' value='Cancel'/>
<% } %>
</div>
</form>
</div>
</div>

View file

@ -0,0 +1,4 @@
<div class='comments'>
<ul>
</ul>
</div>

View file

@ -0,0 +1,18 @@
<div class='global-comment-list'>
<ul><!--
--><% for (let post of ctx.response.results) { %><!--
--><li><!--
--><div class='post-thumbnail'><!--
--><% if (ctx.canViewPosts) { %><!--
--><a href='<%- ctx.formatClientLink('post', post.id) %>'><!--
--><% } %><!--
--><%= ctx.makeThumbnail(post.thumbnailUrl) %><!--
--><% if (ctx.canViewPosts) { %><!--
--></a><!--
--><% } %><!--
--></div><!--
--><div class='comments-container' data-for='<%- post.id %>'></div><!--
--></li><!--
--><% } %><!--
--></ul>
</div>

View file

@ -0,0 +1,7 @@
<div class='pager'>
<div class='page-header-holder'></div>
<div class='messages'></div>
<div class='page-guard top'></div>
<div class='pages-holder'></div>
<div class='page-guard bottom'></div>
</div>

View file

@ -0,0 +1,4 @@
<div class='page'>
<p class='page-header'><span>Page <%- ctx.page %> of <%- ctx.totalPages %></span></p>
<div class='page-content-holder'></div>
</div>

10
client/html/expander.tpl Normal file
View file

@ -0,0 +1,10 @@
<section class='expander'>
<header>
<a href>
<span><%- ctx.title %></span>
<i class='fa fa-chevron-down'></i>
</a>
</header>
<div class='expander-content'></div>
</section>

15
client/html/fav.tpl Normal file
View file

@ -0,0 +1,15 @@
<% if (ctx.canFavorite) { %>
<% if (ctx.ownFavorite) { %>
<a href class='remove-favorite'>
<i class='fa fa-heart'></i>
<% } else { %>
<a href class='add-favorite'>
<i class='fa fa-heart-o'></i>
<% } %>
<% } else { %>
<a class='add-favorite inactive'>
<i class='fa fa-heart-o'></i>
<% } %>
<span class='vim-nav-hint'>add to favorites</span>
</a>
<span class='value'><%- ctx.favoriteCount %></span>

View file

@ -0,0 +1,26 @@
<div class='file-dropper-holder'>
<input type='file' id='<%- ctx.id %>'/>
<label class='file-dropper' for='<%- ctx.id %>' role='button'>
<% if (ctx.allowMultiple) { %>
Drop files here!
<% } else { %>
Drop file here!
<% } %>
<br/>
Or just click on this box.
<% if (ctx.extraText) { %>
<br/>
<small><%= ctx.extraText %></small>
<% } %>
</label>
<% if (ctx.allowUrls) { %>
<div class='url-holder'>
<input type='text' name='url' placeholder='<%- ctx.urlPlaceholder %>'/>
<% if (ctx.lock) { %>
<button>Confirm</button>
<% } else { %>
<button>Add URL</button>
<% } %>
</div>
<% } %>
</div>

13
client/html/help.tpl Normal file
View file

@ -0,0 +1,13 @@
<div class='content-wrapper' id='help'>
<nav class='buttons primary'><!--
--><ul><!--
--><li data-name='about'><a href='<%- ctx.formatClientLink('help', 'about') %>'>About</a></li><!--
--><li data-name='keyboard'><a href='<%- ctx.formatClientLink('help', 'keyboard') %>'>Keyboard</a></li><!--
--><li data-name='search'><a href='<%- ctx.formatClientLink('help', 'search') %>'>Search syntax</a></li><!--
--><li data-name='comments'><a href='<%- ctx.formatClientLink('help', 'comments') %>'>Comments</a></li><!--
--><li data-name='tos'><a href='<%- ctx.formatClientLink('help', 'tos') %>'>Terms of service</a></li><!--
--></ul><!--
--></nav>
<div class='content'></div>
</div>

View file

@ -0,0 +1,13 @@
<p>Szurubooru is an image board engine inspired by services such as Danbooru,
Gelbooru and Moebooru. Its name <a href='http://sjp.pwn.pl/sjp/;2527372'>has
its roots in Polish language and has onomatopeic meaning of scraping or
scrubbing</a>. It is pronounced as <em>shoorubooru</em>.</p>
<p class='section'><strong>Registration</strong></p>
<p>The e-mail you enter during account creation is only used to retrieve your
Gravatar and for password reminders. Only you can see it (well, except the
database staff&hellip; we won&rsquo;t spam your mailbox anyway).</p>
<p>Oh, and you can delete your account at any time. Posts you uploaded will
stay, unless some angry admin removes them.</p>

View file

@ -0,0 +1,38 @@
<p>Comments support Markdown syntax, extended by some handy tags:</p>
<table>
<tbody>
<tr>
<td><code>@426</code></td>
<td>links to post number 426</td>
</tr>
<tr>
<td><code>#Dragon_Ball</code></td>
<td>links to tag &ldquo;Dragon_Ball&rdquo;</td>
</tr>
<tr>
<td><code>+Pirate</code></td>
<td>links to user &ldquo;Pirate&rdquo;</td>
</tr>
<tr>
<td><code>~~new~~</code></td>
<td>adds strike-through</td>
</tr>
<tr>
<td><code>[spoiler]Lelouch survives[/spoiler]</td>
<td>marks text as spoiler and hides it</td>
</tr>
<tr>
<td><code>[sjis](´・ω・`)[/sjis]</td>
<td>adds SJIS art</td>
</tr>
</tbody>
</table>
<p>You can also specify the size of embedded images like this:</p>
<ul>
<li><code>![alt](href =WIDTHx "title")</code></li>
<li><code>![alt](href =xHEIGHT "title")</code></li>
<li><code>![alt](href =WIDTHxHEIGHT "title")</code></li>
</ul>

View file

@ -0,0 +1,47 @@
<p>You can use your keyboard to navigate around the site. There are a few
shortcuts:</p>
<table>
<thead>
<tr>
<th>Hotkey</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Q</kbd></td>
<td>Focus search field, if available</td>
</tr>
<tr>
<td><kbd>A</kbd> and <kbd>D</kbd>, <kbd>←</kbd> and <kbd>→</kbd></td>
<td>Go to newer/older page or post</td>
</tr>
<tr>
<td><kbd>F</kbd></td>
<td>Cycle post fit mode</td>
</tr>
<tr>
<td><kbd>E</kbd></td>
<td>Edit post</td>
</tr>
<tr>
<td><kbd>P</kbd></td>
<td>Focus first post in post list</td>
</tr>
<tr>
<td><kbd>Delete</kbd></td>
<td>Delete post (while in edit mode)</td>
</tr>
</tbody>
</table>
<p>Additionally, each item in the top navigation can be accessed using a
feature called &ldquo;access keys&rdquo;. Pressing the underlined letter while
holding Shift or Alt+Shift (depending on your browser) will go to the desired
page (most browsers) or focus the link (IE).</p>

View file

@ -0,0 +1,11 @@
<nav class='buttons secondary'><!--
--><ul><!--
--><li data-name='default'><a href='<%- ctx.formatClientLink('help', 'search') %>'>General</a></li><!--
--><li data-name='posts'><a href='<%- ctx.formatClientLink('help', 'search', 'posts') %>'>Posts</a></li><!--
--><li data-name='users'><a href='<%- ctx.formatClientLink('help', 'search', 'users') %>'>Users</a></li><!--
--><li data-name='tags'><a href='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Tags</a></li><!--
--><li data-name='pools'><a href='<%- ctx.formatClientLink('help', 'search', 'pools') %>'>Pools</li><!--
--></ul><!--
--></nav>
<div class='subcontent'></div>

View file

@ -0,0 +1,99 @@
<p>Search queries are built of tokens that are separated by spaces. Each token
can be of following form:</p>
<table>
<thead>
<tr>
<th>Syntax</th>
<th>Token type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>&lt;value&gt;</code></td>
<td>anonymous tokens</td>
<td>used for basic filters</td>
</tr>
<tr>
<td><code>&lt;key&gt;:&lt;value&gt;</code></td>
<td>named tokens</td>
<td>used for advanced filters</td>
</tr>
<tr>
<td><code>sort:&lt;style&gt;</code></td>
<td>sort style tokens</td>
<td>used to sort the results</td>
</tr>
<tr>
<td><code>special:&lt;value&gt;</code></td>
<td>special tokens</td>
<td>filters usually tied to the logged in user</td>
</tr>
</tbody>
</table>
<p>Most of anonymous and named tokens support ranged and composite values that
take following form:</p>
<table>
<tbody>
<tr>
<td><code>a,b,c</code></td>
<td>will show things that satisfy either <code>a</code>,
<code>b</code> or <code>c</code>.</td>
</tr>
<tr>
<td><code>1..</code></td>
<td>will show things that are equal to or greater than 1.</td>
</tr>
<tr>
<td><code>..4</code></td>
<td>will show things that are equal to at most 4.</td>
</tr>
<tr>
<td><code>1..4</code></td>
<td>will show things that are equal to 1, 2, 3 or 4.</td>
</tr>
</tbody>
</table>
<p>Ranged values can be also supplied by appending <code>-min</code> or
<code>-max</code> to the key, for example like this:
<code>score-min:1</code>.</p>
<p>Date/time values can be of following form:</p>
<ul>
<li><code>today</code></li>
<li><code>yesterday</code></li>
<li><code>&lt;year&gt;</code></li>
<li><code>&lt;year&gt;-&lt;month&gt;</code></li>
<li><code>&lt;year&gt;-&lt;month&gt;-&lt;day&gt;</code></li>
</ul>
<p>Some fields, such as user names, can take wildcards (<code>*</code>).</p>
<p>All tokens can be negated by prepending them with <code>-</code>.</p>
<p>Sort style token values can be appended with <code>,asc</code> or
<code>,desc</code> to control the sort direction, which can be also controlled
by negating the whole token.</p>
<p>You can escape special characters such as <code>:</code> and <code>-</code>
by prepending them with a backslash: <code>\\</code>.</p>
<h1>Example</h1>
<p>Searching for posts with following query:</p>
<pre><code>sea -fav-count:8.. type:swf uploader:Pirate</code></pre>
<p>will show flash files tagged as sea, that were liked by seven people at
most, uploaded by user Pirate.</p>
<p>Searching for posts with <code>re:zero</code> will show an error message
about unknown named token.</p>
<p>Searching for posts with <code>re\:zero</code> will show posts tagged with
<code>re:zero</code>.</p>

View file

@ -0,0 +1,97 @@
<p><strong>Anonymous tokens</strong></p>
<p>Same as <code>name</code> token.</p>
<p><strong>Named tokens</strong></p>
<table>
<tbody>
<tr>
<td><code>name</code></td>
<td>having given name (accepts wildcards)</td>
</tr>
<tr>
<td><code>category</code></td>
<td>having given category (accepts wildcards)</td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>created at given date</td>
</tr>
<tr>
<td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>last-edit-date</code></td>
<td>edited at given date</td>
</tr>
<tr>
<td><code>last-edit-time</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>edit-date</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>edit-time</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>post-count</code></td>
<td>alias of <code>usages</code></td>
</tr>
</tbody>
</table>
<p><strong>Sort style tokens</strong></p>
<table>
<tbody>
<tr>
<td><code>random</code></td>
<td>as random as it can get</td>
</tr>
<tr>
<td><code>name</code></td>
<td>A to Z</td>
</tr>
<tr>
<td><code>category</code></td>
<td>category (A to Z)</td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>recently created first</td>
</tr>
<tr>
<td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>last-edit-date</code></td>
<td>recently edited first</td>
</tr>
<tr>
<td><code>last-edit-time</code></td>
<td>alias of <code>creation-time</code></td>
</tr>
<tr>
<td><code>edit-date</code></td>
<td>alias of <code>creation-time</code></td>
</tr>
<tr>
<td><code>edit-time</code></td>
<td>alias of <code>creation-time</code></td>
</tr>
<tr>
<td><code>post-count</code></td>
<td>number of posts</td>
</tr>
</tbody>
</table>
<p><strong>Special tokens</strong></p>
<p>None.</p>

View file

@ -0,0 +1,356 @@
<p><strong>Anonymous tokens</strong></p>
<p>Same as <code>tag</code> token.</p>
<p><strong>Named tokens</strong></p>
<table>
<tbody>
<tr>
<td><code>id</code></td>
<td>having given post number</td>
</tr>
<tr>
<td><code>tag</code></td>
<td>having given tag (accepts wildcards)</td>
</tr>
<tr>
<td><code>score</code></td>
<td>having given score</td>
</tr>
<tr>
<td><code>uploader</code></td>
<td>uploaded by given user (accepts wildcards)</td>
</tr>
<tr>
<td><code>upload</code></td>
<td>alias of <code>uploader</code></td>
</tr>
<tr>
<td><code>submit</code></td>
<td>alias of <code>uploader</code></td>
</tr>
<tr>
<td><code>comment</code></td>
<td>commented by given user (accepts wildcards)</td>
</tr>
<tr>
<td><code>fav</code></td>
<td>favorited by given user (accepts wildcards)</td>
</tr>
<tr>
<td><code>source</code></td>
<td>having given source URL (accepts wildcards)</td>
</tr>
<tr>
<td><code>pool</code></td>
<td>belonging to the pool with the given ID</td>
</tr>
<tr>
<td><code>tag-count</code></td>
<td>having given number of tags</td>
</tr>
<tr>
<td><code>comment-count</code></td>
<td>having given number of comments</td>
</tr>
<tr>
<td><code>fav-count</code></td>
<td>favorited by given number of users</td>
</tr>
<tr>
<td><code>note-count</code></td>
<td>having given number of annotations</td>
</tr>
<tr>
<td><code>note-text</code></td>
<td>having given note text (accepts wildcards)</td>
</tr>
<tr>
<td><code>relation-count</code></td>
<td>having given number of relations</td>
</tr>
<tr>
<td><code>feature-count</code></td>
<td>having been featured given number of times</td>
</tr>
<tr>
<td><code>type</code></td>
<td>given type of posts. <code>&lt;value&gt;</code> can be either <code>image</code>, <code>animation</code> (or <code>animated</code> or <code>anim</code>), <code>flash</code> (or <code>swf</code>) or <code>video</code> (or <code>webm</code>).</td>
</tr>
<tr>
<td><code>flag</code></td>
<td>having given flag. <code>&lt;value&gt;</code> can be either <code>loop</code> or <code>sound</code>.</td>
</tr>
<tr>
<td><code>sha1</code></td>
<td>having given SHA1 checksum</td>
</tr>
<tr>
<td><code>md5</code></td>
<td>having given MD5 checksum</td>
</tr>
<tr>
<td><code>content-checksum</code></td>
<td>alias of <code>sha1</code></td>
</tr>
<tr>
<td><code>file-size</code></td>
<td>having given file size (in bytes)</td>
</tr>
<tr>
<td><code>image-width</code></td>
<td>having given image width (where applicable)</td>
</tr>
<tr>
<td><code>image-height</code></td>
<td>having given image height (where applicable)</td>
</tr>
<tr>
<td><code>image-area</code></td>
<td>having given number of pixels (image width * image height)</td>
</tr>
<tr>
<td><code>image-aspect-ratio</code></td>
<td>having given aspect ratio (image width / image height)</td>
</tr>
<tr>
<td><code>image-ar</code></td>
<td>alias of <code>image-aspect-ratio</code></td>
</tr>
<tr>
<td><code>width</code></td>
<td>alias of <code>image-width</code></td>
</tr>
<tr>
<td><code>height</code></td>
<td>alias of <code>image-height</code></td>
</tr>
<tr>
<td><code>area</code></td>
<td>alias of <code>image-area</code></td>
</tr>
<tr>
<td><code>aspect-ratio</code></td>
<td>alias of <code>image-aspect-ratio</code></td>
</tr>
<tr>
<td><code>ar</code></td>
<td>alias of <code>image-aspect-ratio</code></td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>posted at given date</td>
</tr>
<tr>
<td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>date</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>last-edit-date</code></td>
<td>edited at given date</td>
</tr>
<tr>
<td><code>last-edit-time</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>edit-date</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>edit-time</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>comment-date</code></td>
<td>commented at given date</td>
</tr>
<tr>
<td><code>comment-time</code></td>
<td>alias of <code>comment-date</code></td>
</tr>
<tr>
<td><code>fav-date</code></td>
<td>last favorited at given date</td>
</tr>
<tr>
<td><code>fav-time</code></td>
<td>alias of <code>fav-date</code></td>
</tr>
<tr>
<td><code>feature-date</code></td>
<td>featured at given date</td>
</tr>
<tr>
<td><code>feature-time</code></td>
<td>alias of <code>feature-time</code></td>
</tr>
<tr>
<td><code>safety</code></td>
<td>having given safety</td>
</tr>
<tr>
<td><code>rating</code></td>
<td>alias of <code>safety</code></td>
</tr>
</tbody>
</table>
<p><strong>Sort style tokens</strong></p>
<table>
<tbody>
<tr>
<td><code>random</code></td>
<td>as random as it can get</td>
</tr>
<tr>
<td><code>id</code></td>
<td>highest to lowest post number</td>
</tr>
<tr>
<td><code>score</code></td>
<td>highest scored</td>
</tr>
<tr>
<td><code>tag-count</code></td>
<td>with most tags</td>
</tr>
<tr>
<td><code>comment-count</code></td>
<td>most commented first</td>
</tr>
<tr>
<td><code>fav-count</code></td>
<td>loved by most</td>
</tr>
<tr>
<td><code>note-count</code></td>
<td>with most annotations</td>
</tr>
<tr>
<td><code>relation-count</code></td>
<td>with most relations</td>
</tr>
<tr>
<td><code>feature-count</code></td>
<td>most often featured</td>
</tr>
<tr>
<td><code>file-size</code></td>
<td>largest files first</td>
</tr>
<tr>
<td><code>image-width</code></td>
<td>widest images first</td>
</tr>
<tr>
<td><code>image-height</code></td>
<td>tallest images first</td>
</tr>
<tr>
<td><code>image-area</code></td>
<td>largest images first</td>
</tr>
<tr>
<td><code>width</code></td>
<td>alias of <code>image-width</code></td>
</tr>
<tr>
<td><code>height</code></td>
<td>alias of <code>image-height</code></td>
</tr>
<tr>
<td><code>area</code></td>
<td>alias of <code>image-area</code></td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>newest to oldest (pretty much same as <code>id</code>)</td>
</tr>
<tr>
<td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>date</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>last-edit-date</code></td>
<td>like <code>creation-date</code>, only looks at last edit time</td>
</tr>
<tr>
<td><code>last-edit-time</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>edit-date</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>edit-time</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>comment-date</code></td>
<td>recently commented by anyone</td>
</tr>
<tr>
<td><code>comment-time</code></td>
<td>alias of <code>comment-date</code></td>
</tr>
<tr>
<td><code>fav-date</code></td>
<td>recently added to favorites by anyone</td>
</tr>
<tr>
<td><code>fav-time</code></td>
<td>alias of <code>fav-date</code></td>
</tr>
<tr>
<td><code>feature-date</code></td>
<td>recently featured</td>
</tr>
<tr>
<td><code>feature-time</code></td>
<td>alias of <code>feature-time</code></td>
</tr>
</tbody>
</table>
<p><strong>Special tokens</strong></p>
<table>
<tbody>
<tr>
<td><code>liked</code></td>
<td>posts liked by currently logged in user</td>
</tr>
<tr>
<td><code>disliked</code></td>
<td>posts disliked by currently logged in user</td>
</tr>
<tr>
<td><code>fav</code></td>
<td>posts added to favorites by currently logged in user</td>
</tr>
<tr>
<td><code>tumbleweed</code></td>
<td>posts with score of 0, without comments and without favorites</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,129 @@
<p><strong>Anonymous tokens</strong></p>
<p>Same as <code>name</code> token.</p>
<p><strong>Named tokens</strong></p>
<table>
<tbody>
<tr>
<td><code>name</code></td>
<td>having given name (accepts wildcards)</td>
</tr>
<tr>
<td><code>category</code></td>
<td>having given category (accepts wildcards)</td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>created at given date</td>
</tr>
<tr>
<td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>last-edit-date</code></td>
<td>edited at given date</td>
</tr>
<tr>
<td><code>last-edit-time</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>edit-date</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>edit-time</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>usages</code></td>
<td>used in given number of posts</td>
</tr>
<tr>
<td><code>usage-count</code></td>
<td>alias of <code>usages</code></td>
</tr>
<tr>
<td><code>post-count</code></td>
<td>alias of <code>usages</code></td>
</tr>
<tr>
<td><code>suggestion-count</code></td>
<td>with given number of suggestions</td>
</tr>
<tr>
<td><code>implication-count</code></td>
<td>with given number of implications</td>
</tr>
</tbody>
</table>
<p><strong>Sort style tokens</strong></p>
<table>
<tbody>
<tr>
<td><code>random</code></td>
<td>as random as it can get</td>
</tr>
<tr>
<td><code>name</code></td>
<td>A to Z</td>
</tr>
<tr>
<td><code>category</code></td>
<td>category (A to Z)</td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>recently created first</td>
</tr>
<tr>
<td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>last-edit-date</code></td>
<td>recently edited first</td>
</tr>
<tr>
<td><code>last-edit-time</code></td>
<td>alias of <code>creation-time</code></td>
</tr>
<tr>
<td><code>edit-date</code></td>
<td>alias of <code>creation-time</code></td>
</tr>
<tr>
<td><code>edit-time</code></td>
<td>alias of <code>creation-time</code></td>
</tr>
<tr>
<td><code>usages</code></td>
<td>used in most posts first</td>
</tr>
<tr>
<td><code>usage-count</code></td>
<td>alias of <code>usages</code></td>
</tr>
<tr>
<td><code>post-count</code></td>
<td>alias of <code>usages</code></td>
</tr>
<tr>
<td><code>suggestion-count</code></td>
<td>with most suggestions first</td>
</tr>
<tr>
<td><code>implication-count</code></td>
<td>with most implications first</td>
</tr>
</tbody>
</table>
<p><strong>Special tokens</strong></p>
<p>None.</p>

View file

@ -0,0 +1,81 @@
<p><strong>Anonymous tokens</strong></p>
<p>Same as <code>name</code> token.</p>
<p><strong>Named tokens</strong></p>
<table>
<tbody>
<tr>
<td><code>name</code></td>
<td>having given name (accepts wildcards)</td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>registered at given date</td>
</tr>
<tr>
<td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>last-login-date</code>
<td>whose most recent login date matches given date</td>
</tr>
<tr>
<td><code>last-login-time</code>
<td>alias of <code>last-login-date</code>
</tr>
<tr>
<td><code>login-date</code>
<td>alias of <code>last-login-date</code>
</tr>
<tr>
<td><code>login-time</code></td>
<td>alias of <code>last-login-date</code>
</tr>
</tbody>
</table>
<p><strong>Sort style tokens</strong></p>
<table>
<tbody>
<tr>
<td><code>random</code></td>
<td>as random as it can get</td>
</tr>
<tr>
<td><code>name</code></td>
<td>A to Z</td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>newest to oldest</td>
</tr>
<tr>
<td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>last-login-date</code></td>
<td>recently active first</td>
</tr>
<tr>
<td><code>last-login-time</code></td>
<td>alias of <code>last-login-date</code></td>
</tr>
<tr>
<td><code>login-date</code></td>
<td>alias of <code>last-login-date</code></td>
</tr>
<tr>
<td><code>login-time</code></td>
<td>alias of <code>last-login-date</code></td>
</tr>
</tbody>
</table>
<p><strong>Special tokens</strong></p>
<p>None.</p>

51
client/html/help_tos.tpl Normal file
View file

@ -0,0 +1,51 @@
<p>By accessing <%- ctx.name %> (&ldquo;Site&rdquo;) you agree to the following
Terms of Service. If you do not agree to these terms, then please do not access
the Site.</p>
<ul>
<li>The Site is presented to you AS IS, without any warranty, express or
implied. You will not hold the Site or its staff members liable for damages
caused by the use of the site.</li>
<li>The Site reserves the right to delete or modify your account, or any
content you have posted to the site.</li>
<li>The Site reserves the right to change these Terms of Service without
prior notice.</li>
<li>If you are a minor, then you will not use the Site.</li>
<li>You are using the Site only for personal use.</li>
<li>You will not spam, troll or offend anyone.</li>
<li>You accept that the Site is not liable for any content that you may
stumble upon.</li>
</ul>
<p class='section' id='section-prohibited-content'><strong>Prohibited content</strong></p>
<ul>
<li>Child pornography: any photograph or photorealistic drawing or movie
that depicts children in a sexual manner. This includes nudity, explicit
sex, implied sex, or sexually persuasive positions.</li>
<li>Bestiality: any photograph or photorealistic drawing or movie that
depicts humans having sex (either explicit or implied) with other non-human
animals.</li>
<li>Any depiction of extreme mutilation, extreme bodily distension,
feces.</li>
<li>Personal images: any image that is suspected to be uploaded for
personal use. This includes, but is not limited to, avatars and forum
signatures.</li>
</ul>
<p class='section' id='section-privacy-policy'><strong>Privacy policy</strong></p>
<p>The Site will not disclose the IP address or email address of any user
except to the staff.</p>
<p>Posts, comments, favorites, ratings and other actions linked to your account
will be stored in the Site&rsquo;s database. The &ldquo;Upload
anonymously&rdquo; option allows you to post content without linking it to your
account&nbsp;&ndash; meaning your nickname will not be stored in the database
nor shown in the &ldquo;Uploader&rdquo; field.</p>
<p>Cookies are used to store your session data in order to keep you logged in
and personalize your web experience.</p>

16
client/html/home.tpl Normal file
View file

@ -0,0 +1,16 @@
<div class='content-wrapper transparent' id='home'>
<div class='messages'></div>
<header>
<h1><%- ctx.name %></h1>
</header>
<% if (ctx.canListPosts) { %>
<form class='horizontal'>
<%= ctx.makeTextInput({name: 'search-text', placeholder: 'enter some tags'}) %>
<input type='submit' value='Search'/>
<span class=sep>or</span>
<a href='<%- ctx.formatClientLink('posts') %>'>browse all posts</a>
</form>
<% } %>
<div class='post-info-container'></div>
<footer class='footer-container'></footer>
</div>

View file

@ -0,0 +1,7 @@
<div class='post-container'></div>
<% if (ctx.featuredPost) { %>
<aside>
Featured&nbsp;post:&nbsp;<%= ctx.makePostLink(ctx.featuredPost.id, true) %>,<wbr>
posted&nbsp;<%= ctx.makeRelativeTime(ctx.featuredPost.creationTime) %>&nbsp;by&nbsp;<%= ctx.makeUserLink(ctx.featuredPost.user) %>
</aside>
<% } %>

View file

@ -0,0 +1,7 @@
<ul>
<li><%- ctx.postCount %> posts</li><span class='sep'>
</span><li><%= ctx.makeFileSize(ctx.diskUsage) %></li><span class='sep'>
</span><li>Build <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%- ctx.version %></a><%- ctx.isDevelopmentMode ? " (DEV MODE)" : "" %> from <%= ctx.makeRelativeTime(ctx.buildDate) %></li><span class='sep'>
</span><% if (ctx.canListSnapshots) { %><li><a href='<%- ctx.formatClientLink('history') %>'>History</a></li><span class='sep'>
</span><% } %>
</ul>

32
client/html/index.htm Normal file
View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name='theme-color' content='#24aadd'/>
<meta name='apple-mobile-web-app-capable' content='yes'/>
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>
<meta name='msapplication-TileColor' content='#ffffff'/>
<meta name="msapplication-TileImage" content="/img/mstile-150x150.png">
<title>Loading...</title>
<!-- Base HTML Placeholder -->
<link href='css/app.min.css' rel='stylesheet' type='text/css'/>
<link href='css/vendor.min.css' rel='stylesheet' type='text/css'/>
<link rel='shortcut icon' type='image/png' href='img/favicon.png'/>
<link rel='apple-touch-icon' sizes='180x180' href='img/apple-touch-icon.png'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-640x1136.png' media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-750x1294.png' media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1242x2148.png' media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1125x2436.png' media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1536x2048.png' media='(min-device-width: 768px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1668x2224.png' media='(min-device-width: 834px) and (max-device-width: 834px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-2048x2732.png' media='(min-device-width: 1024px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='manifest' href='manifest.json'/>
</head>
<body>
<div id='top-navigation-holder'></div>
<div id='content-holder'></div>
<script type='text/javascript' src='js/vendor.min.js'></script>
<script type='text/javascript' src='js/app.min.js'></script>
</body>
</html>

36
client/html/login.tpl Normal file
View file

@ -0,0 +1,36 @@
<div class='content-wrapper' id='login'>
<h1>Log in</h1>
<form>
<ul class='input'>
<li>
<%= ctx.makeTextInput({
text: 'User name',
name: 'name',
required: true,
pattern: ctx.userNamePattern,
}) %>
</li>
<li>
<%= ctx.makePasswordInput({
text: 'Password',
name: 'password',
required: true,
pattern: ctx.passwordPattern,
}) %>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Remember me',
name: 'remember-user',
}) %>
</li>
</ul>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Log in'/>
<a class='append' href='<%- ctx.formatClientLink('password-reset') %>'>Forgot the password?</a>
</div>
</form>
</div>

View file

@ -0,0 +1,6 @@
<div class='pager'>
<div class='page-header-holder'></div>
<div class='messages'></div>
<div class='page-content-holder'></div>
<div class='page-nav'></div>
</div>

View file

@ -0,0 +1,39 @@
<nav class='buttons'>
<ul>
<li>
<% if (ctx.prevPage !== ctx.currentPage) { %>
<a rel='prev' class='prev' href='<%- ctx.getClientUrlForPage(ctx.pages.get(ctx.prevPage).offset, ctx.pages.get(ctx.prevPage).limit) %>'>
<% } else { %>
<a rel='prev' class='prev disabled'>
<% } %>
<i class='fa fa-chevron-left'></i>
<span class='vim-nav-hint'>&lt; Previous page</span>
</a>
</li>
<% for (let page of ctx.pages.values()) { %>
<% if (page.ellipsis) { %>
<li>&hellip;</li>
<% } else { %>
<% if (page.active) { %>
<li class='active'>
<% } else { %>
<li>
<% } %>
<a href='<%- ctx.getClientUrlForPage(page.offset, page.limit) %>'><%- page.number %></a>
</li>
<% } %>
<% } %>
<li>
<% if (ctx.nextPage !== ctx.currentPage) { %>
<a rel='next' class='next' href='<%- ctx.getClientUrlForPage(ctx.pages.get(ctx.nextPage).offset, ctx.pages.get(ctx.nextPage).limit) %>'>
<% } else { %>
<a rel='next' class='next disabled'>
<% } %>
<i class='fa fa-chevron-right'></i>
<span class='vim-nav-hint'>Next page &gt;</span>
</a>
</li>
</ul>
</nav>

View file

@ -0,0 +1,5 @@
<div class='not-found'>
<h1>Not found</h1>
<p><%- ctx.path %> is not a valid URL.</p>
<p><a href='<%- ctx.formatClientLink() %>'>Back to main page</a></p>
</div>

View file

@ -0,0 +1,30 @@
<div class='content-wrapper' id='password-reset'>
<h1>Password reset</h1>
<% if (ctx.canSendMails) { %>
<form autocomplete='off'>
<ul class='input'>
<li>
<%= ctx.makeTextInput({
text: 'User name or e-mail address',
name: 'user-name',
required: true,
}) %>
</li>
</ul>
<p><small>Proceeding will send an e-mail that contains a password reset
link. Clicking it is going to generate a new password for your account.
It is recommended to change that password to something else.</small></p>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Proceed'/>
</div>
</form>
<% } else { %>
<p>We do not support automatic password resetting.</p>
<% if (ctx.contactEmail) { %>
<p>Please send an e-mail to <a href='mailto:<%- ctx.contactEmail %>'><%- ctx.contactEmail %></a> to go through a manual procedure.</p>
<% } %>
<% } %>
</div>

18
client/html/pool.tpl Normal file
View file

@ -0,0 +1,18 @@
<div class='content-wrapper' id='pool'>
<h1><%- ctx.getPrettyName(ctx.pool.names[0]) %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li data-name='summary'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'edit') %>'>Edit</a></li><!--
--><% } %><!--
--><% if (ctx.canMerge) { %><!--
--><li data-name='merge'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'merge') %>'>Merge with&hellip;</a></li><!--
--><% } %><!--
--><% if (ctx.canDelete) { %><!--
--><li data-name='delete'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'delete') %>'>Delete</a></li><!--
--><% } %><!--
--></ul><!--
--></nav>
<div class='pool-content-holder'></div>
</div>

View file

@ -0,0 +1,30 @@
<div class='content-wrapper pool-categories'>
<form>
<h1>Pool categories</h1>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class='name'>Category name</th>
<th class='color'>CSS color</th>
<th class='usages'>Usages</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<% if (ctx.canCreate) { %>
<p><a href class='add'>Add new category</a></p>
<% } %>
<div class='messages'></div>
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canDelete) { %>
<div class='buttons'>
<input type='submit' class='save' value='Save changes'>
</div>
<% } %>
</form>
</div>

View file

@ -0,0 +1,43 @@
<% if (ctx.poolCategory.isDefault) { %><%
%><tr data-category='<%- ctx.poolCategory.name %>' class='default'><%
%><% } else { %><%
%><tr data-category='<%- ctx.poolCategory.name %>'><%
%><% } %>
<td class='name'>
<% if (ctx.canEditName) { %>
<%= ctx.makeTextInput({value: ctx.poolCategory.name, required: true}) %>
<% } else { %>
<%- ctx.poolCategory.name %>
<% } %>
</td>
<td class='color'>
<% if (ctx.canEditColor) { %>
<%= ctx.makeColorInput({value: ctx.poolCategory.color}) %>
<% } else { %>
<%- ctx.poolCategory.color %>
<% } %>
</td>
<td class='usages'>
<% if (ctx.poolCategory.name) { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'category:' + ctx.poolCategory.name}) %>'>
<%- ctx.poolCategory.poolCount %>
</a>
<% } else { %>
<%- ctx.poolCategory.poolCount %>
<% } %>
</td>
<% if (ctx.canDelete) { %>
<td class='remove'>
<% if (ctx.poolCategory.poolCount) { %>
<a class='inactive' title="Can't delete category in use">Remove</a>
<% } else { %>
<a href>Remove</a>
<% } %>
</td>
<% } %>
<% if (ctx.canSetDefault) { %>
<td class='set-default'>
<a href>Make default</a>
</td>
<% } %>
</tr>

View file

@ -0,0 +1,42 @@
<div class='content-wrapper pool-create'>
<form>
<ul class='input'>
<li class='names'>
<%= ctx.makeTextInput({
text: 'Names',
value: '',
required: true,
}) %>
</li>
<li class='category'>
<%= ctx.makeSelect({
text: 'Category',
keyValues: ctx.categories,
selectedKey: 'default',
required: true,
}) %>
</li>
<li class='description'>
<%= ctx.makeTextarea({
text: 'Description',
value: '',
}) %>
</li>
<li class='posts'>
<%= ctx.makeTextInput({
text: 'Posts',
value: '',
placeholder: 'space-separated post IDs',
}) %>
</li>
</ul>
<% if (ctx.canCreate) { %>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' class='save' value='Create pool'>
</div>
<% } %>
</form>
</div>

View file

@ -0,0 +1,21 @@
<div class='pool-delete'>
<form>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
<ul class='input'>
<li>
<%= ctx.makeCheckbox({
name: 'confirm-deletion',
text: 'I confirm that I want to delete this pool.',
required: true,
}) %>
</li>
</ul>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Delete pool'/>
</div>
</form>
</div>

50
client/html/pool_edit.tpl Normal file
View file

@ -0,0 +1,50 @@
<div class='content-wrapper pool-edit'>
<form>
<ul class='input'>
<li class='names'>
<% if (ctx.canEditNames) { %>
<%= ctx.makeTextInput({
text: 'Names',
value: ctx.pool.names.join(' '),
required: true,
}) %>
<% } %>
</li>
<li class='category'>
<% if (ctx.canEditCategory) { %>
<%= ctx.makeSelect({
text: 'Category',
keyValues: ctx.categories,
selectedKey: ctx.pool.category,
required: true,
}) %>
<% } %>
</li>
<li class='description'>
<% if (ctx.canEditDescription) { %>
<%= ctx.makeTextarea({
text: 'Description',
value: ctx.pool.description,
}) %>
<% } %>
</li>
<li class='posts'>
<% if (ctx.canEditPosts) { %>
<%= ctx.makeTextInput({
text: 'Posts',
placeholder: 'space-separated post IDs',
value: ctx.pool.posts.map(post => post.id).join(' ')
}) %>
<% } %>
</li>
</ul>
<% if (ctx.canEditAnything) { %>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' class='save' value='Save changes'>
</div>
<% } %>
</form>
</div>

View file

@ -0,0 +1,7 @@
<div class='pool-input'>
<div class='main-control'>
<input type='text' placeholder='type to add…'/>
</div>
<ul class='compact-pools'></ul>
</div>

View file

@ -0,0 +1,22 @@
<div class='pool-merge'>
<form>
<ul class='input'>
<li class='target'>
<%= ctx.makeTextInput({name: 'target-pool', required: true, text: 'Target pool', pattern: ctx.poolNamePattern}) %>
</li>
<li>
<p>Posts in the two pools will be combined.
Category needs to be handled manually.</p>
<%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this pool.'}) %>
</li>
</ul>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Merge pool'/>
</div>
</form>
</div>

View file

@ -0,0 +1,23 @@
<div class='content-wrapper pool-summary'>
<section class='details'>
<section>
Category:
<span class='<%= ctx.makeCssName(ctx.pool.category, 'pool') %>'><%- ctx.pool.category %></span>
</section>
<section>
Aliases:<br/>
<ul><!--
--><% for (let name of ctx.pool.names.slice(1)) { %><!--
--><li><%= ctx.makePoolLink(ctx.pool.id, false, false, ctx.pool, name) %></li><!--
--><% } %><!--
--></ul>
</section>
</section>
<section class='description'>
<hr/>
<%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
</section>
</div>

View file

@ -0,0 +1,22 @@
<div class='pool-list-header'>
<form class='horizontal'>
<ul class='input'>
<li>
<%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %>
</li>
</ul>
<div class='buttons'>
<input type='submit' value='Search'/>
<a class='button append' href='<%- ctx.formatClientLink('help', 'search', 'pools') %>'>Syntax help</a>
<% if (ctx.canCreate) { %>
<a class='append' href='<%- ctx.formatClientLink('pool', 'create') %>'>Add new pool</a>
<% } %>
<% if (ctx.canEditPoolCategories) { %>
<a class='append' href='<%- ctx.formatClientLink('pool-categories') %>'>Pool categories</a>
<% } %>
</div>
</form>
</div>

View file

@ -0,0 +1,48 @@
<div class='pool-list table-wrap'>
<% if (ctx.response.results.length) { %>
<table>
<thead>
<th class='names'>
<% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:name'}) %>'>Pool name(s)</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:name'}) %>'>Pool name(s)</a>
<% } %>
</th>
<th class='post-count'>
<% if (ctx.parameters.query == 'sort:post-count') { %>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:post-count'}) %>'>Post count</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:post-count'}) %>'>Post count</a>
<% } %>
</th>
<th class='creation-time'>
<% if (ctx.parameters.query == 'sort:creation-time') { %>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:creation-time'}) %>'>Created on</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:creation-time'}) %>'>Created on</a>
<% } %>
</th>
</thead>
<tbody>
<% for (let pool of ctx.response.results) { %>
<tr>
<td class='names'>
<ul>
<% for (let name of pool.names) { %>
<li><%= ctx.makePoolLink(pool.id, false, false, pool, name) %></li>
<% } %>
</ul>
</td>
<td class='post-count'>
<a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + pool.id}) %>'><%- pool.postCount %></a>
</td>
<td class='creation-time'>
<%= ctx.makeRelativeTime(pool.creationTime) %>
</td>
</tr>
<% } %>
</tbody>
</table>
<% } %>
</div>

View file

@ -0,0 +1,34 @@
<div class='post-content post-type-<%- ctx.post.type %>'>
<% if (['image', 'animation'].includes(ctx.post.type)) { %>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>'/>
<% } else if (ctx.post.type === 'flash') { %>
<object class='resize-listener' width='<%- ctx.post.canvasWidth %>' height='<%- ctx.post.canvasHeight %>' data='<%- ctx.post.contentUrl %>'>
<param name='wmode' value='opaque'/>
<param name='movie' value='<%- ctx.post.contentUrl %>'/>
</object>
<% } else if (ctx.post.type === 'video') { %>
<%= ctx.makeElement(
'video', {
class: 'resize-listener',
controls: true,
loop: (ctx.post.flags || []).includes('loop'),
playsinline: true,
autoplay: ctx.autoplay,
},
ctx.makeElement('source', {
type: ctx.post.mimeType,
src: ctx.post.contentUrl,
}),
'Your browser doesn\'t support HTML5 videos.')
%>
<% } else { console.log(new Error('Unknown post type')); } %>
<div class='post-overlay resize-listener'>
</div>
</div>

View file

@ -0,0 +1,12 @@
<div class='content-wrapper' id='post'>
<h1>Post #<%- ctx.post.id %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li><a href='<%- ctx.formatClientLink('post', ctx.post.id) %>'><i class='fa fa-reply'></i> Main view</a></li><!--
--><% if (ctx.canMerge) { %><!--
--><li data-name='merge'><a href='<%- ctx.formatClientLink('post', ctx.post.id, 'merge') %>'>Merge with&hellip;</a></li><!--
--><% } %><!--
--></ul><!--
--></nav>
<div class='post-content-holder'></div>
</div>

View file

@ -0,0 +1,127 @@
<div class='edit-sidebar'>
<form autocomplete='off'>
<input type='submit' value='Save' class='submit'/>
<div class='messages'></div>
<% if (ctx.enableSafety && ctx.canEditPostSafety) { %>
<section class='safety'>
<label>Safety</label>
<div class='radio-wrapper'>
<%= ctx.makeRadio({
name: 'safety',
class: 'safety-safe',
value: 'safe',
selectedValue: ctx.post.safety,
text: 'Safe'}) %>
<%= ctx.makeRadio({
name: 'safety',
class: 'safety-sketchy',
value: 'sketchy',
selectedValue: ctx.post.safety,
text: 'Sketchy'}) %>
<%= ctx.makeRadio({
name: 'safety',
value: 'unsafe',
selectedValue: ctx.post.safety,
class: 'safety-unsafe',
text: 'Unsafe'}) %>
</div>
</section>
<% } %>
<% if (ctx.canEditPostRelations) { %>
<section class='relations'>
<%= ctx.makeTextInput({
text: 'Relations',
name: 'relations',
placeholder: 'space-separated post IDs',
pattern: '^[0-9 ]*$',
value: ctx.post.relations.map(rel => rel.id).join(' '),
}) %>
</section>
<% } %>
<% if (ctx.canEditPostFlags && ctx.post.type === 'video') { %>
<section class='flags'>
<label>Miscellaneous</label>
<%= ctx.makeCheckbox({
text: 'Loop video',
name: 'loop',
checked: ctx.post.flags.includes('loop'),
}) %>
<%= ctx.makeCheckbox({
text: 'Sound',
name: 'sound',
checked: ctx.post.flags.includes('sound'),
}) %>
</section>
<% } %>
<% if (ctx.canEditPostSource) { %>
<section class='post-source'>
<%= ctx.makeTextarea({
text: 'Source',
value: ctx.post.source,
}) %>
</section>
<% } %>
<% if (ctx.canEditPostTags) { %>
<section class='tags'>
<%= ctx.makeTextInput({}) %>
</section>
<% } %>
<% if (ctx.canEditPoolPosts) { %>
<section class='pools'>
<%= ctx.makeTextInput({}) %>
</section>
<% } %>
<% if (ctx.canEditPostNotes) { %>
<section class='notes'>
<a href class='add'>Add a note</a>
<%= ctx.makeTextarea({disabled: true, text: 'Content (supports Markdown)', rows: '8'}) %>
<a href class='delete inactive'>Delete selected note</a>
<% if (ctx.hasClipboard) { %>
<br/>
<a href class='copy'>Export notes to clipboard</a>
<br/>
<a href class='paste'>Import notes from clipboard</a>
<% } %>
</section>
<% } %>
<% if (ctx.canEditPostContent) { %>
<section class='post-content'>
<label>Content</label>
<div class='dropper-container'></div>
</section>
<% } %>
<% if (ctx.canEditPostThumbnail) { %>
<section class='post-thumbnail'>
<label>Thumbnail</label>
<div class='dropper-container'></div>
<a href>Discard custom thumbnail</a>
</section>
<% } %>
<% if (ctx.canFeaturePosts || ctx.canDeletePosts || ctx.canMergePosts) { %>
<section class='management'>
<ul>
<% if (ctx.canFeaturePosts) { %>
<li><a href class='feature'>Feature this post on main page</a></li>
<% } %>
<% if (ctx.canMergePosts) { %>
<li><a href class='merge'>Merge this post with another</a></li>
<% } %>
<% if (ctx.canDeletePosts) { %>
<li><a href class='delete'>Delete this post</a></li>
<% } %>
</ul>
</section>
<% } %>
</form>
</div>

66
client/html/post_main.tpl Normal file
View file

@ -0,0 +1,66 @@
<div class='content-wrapper transparent post-view'>
<aside class='sidebar'>
<nav class='buttons'>
<article class='previous-post'>
<% if (ctx.prevPostId) { %>
<% if (ctx.editMode) { %>
<a rel='prev' href='<%= ctx.getPostEditUrl(ctx.prevPostId, ctx.parameters) %>'>
<% } else { %>
<a rel='prev' href='<%= ctx.getPostUrl(ctx.prevPostId, ctx.parameters) %>'>
<% } %>
<% } else { %>
<a rel='prev' class='inactive'>
<% } %>
<i class='fa fa-chevron-left'></i>
<span class='vim-nav-hint'>&lt; Previous post</span>
</a>
</article>
<article class='next-post'>
<% if (ctx.nextPostId) { %>
<% if (ctx.editMode) { %>
<a rel='next' href='<%= ctx.getPostEditUrl(ctx.nextPostId, ctx.parameters) %>'>
<% } else { %>
<a rel='next' href='<%= ctx.getPostUrl(ctx.nextPostId, ctx.parameters) %>'>
<% } %>
<% } else { %>
<a rel='next' class='inactive'>
<% } %>
<i class='fa fa-chevron-right'></i>
<span class='vim-nav-hint'>Next post &gt;</span>
</a>
</article>
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
<article class='edit-post'>
<% if (ctx.editMode) { %>
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
<i class='fa fa-reply'></i>
<span class='vim-nav-hint'>Back to view mode</span>
</a>
<% } else { %>
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
<i class='fa fa-pencil'></i>
<span class='vim-nav-hint'>Edit post</span>
</a>
<% } %>
</article>
<% } %>
</nav>
<div class='sidebar-container'></div>
</aside>
<div class='content'>
<div class='post-container'></div>
<div class='after-mobile-controls'>
<% if (ctx.canCreateComments) { %>
<h2>Add comment</h2>
<div class='comment-form-container'></div>
<% } %>
<% if (ctx.canListComments) { %>
<div class='comments-container'></div>
<% } %>
</div>
</div>
</div>

View file

@ -0,0 +1,23 @@
<div class='post-merge'>
<form>
<ul class='input'>
<li class='post-mirror'>
<div class='left-post-container'></div>
<div class='right-post-container'></div>
</li>
<li>
<p>Tags, relations, scores, favorites and comments will be
merged. All other properties need to be handled manually.</p>
<%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge these posts.'}) %>
</li>
</ul>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Merge posts'/>
</div>
</form>
</div>

View file

@ -0,0 +1,59 @@
<header>
<label for='merge-id-<%- ctx.name %>'>Post #</label>
<% if (ctx.editable) { %>
<input type='text' id='merge-id-<%-ctx.name %>' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>'/>
<input type='button' value='Search'/>
<% } else { %>
<input type='text' id='merge-id-<%-ctx.name %>' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>' readonly/>
<% } %>
</header>
<% if (ctx.post) { %>
<div class='post-thumbnail'>
<a rel='external' href='<%- ctx.post.contentUrl %>'>
<%= ctx.makeThumbnail(ctx.post.thumbnailUrl) %>
</a>
</div>
<div class='target-post'>
<%= ctx.makeRadio({
required: true,
text: 'Merge to this post<br/><small>' +
ctx.makeUserLink(ctx.post.user) +
', ' +
ctx.makeRelativeTime(ctx.post.creationTime) +
'</small>',
name: 'target-post',
value: ctx.name,
}) %>
</div>
<div class='target-post-content'>
<%= ctx.makeRadio({
required: true,
text: 'Use this file<br/><small>' +
ctx.makeFileSize(ctx.post.fileSize) + ' ' +
{
'image/gif': 'GIF',
'image/jpeg': 'JPEG',
'image/png': 'PNG',
'image/webp': 'WEBP',
'image/bmp': 'BMP',
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'video/webm': 'WEBM',
'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] +
' (' +
(ctx.post.canvasWidth ?
`${ctx.post.canvasWidth}x${ctx.post.canvasHeight}` :
'?') +
')</small>',
name: 'target-post-content',
value: ctx.name,
}) %>
<p>
</p>
</div>
<% } %>

View file

@ -0,0 +1,119 @@
<div class='readonly-sidebar'>
<article class='details'>
<section class='download'>
<a rel='external' href='<%- ctx.post.contentUrl %>'>
<i class='fa fa-download'></i><!--
--><%= ctx.makeFileSize(ctx.post.fileSize) %> <!--
--><%- {
'image/gif': 'GIF',
'image/jpeg': 'JPEG',
'image/png': 'PNG',
'image/webp': 'WEBP',
'image/bmp': 'BMP',
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'video/webm': 'WEBM',
'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] %><!--
--></a>
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
<% if (ctx.post.flags.length) { %><!--
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
--><% if (ctx.post.flags.includes('sound')) { %><i class='fa fa-volume-up'></i><% } %>
<% } %>
</section>
<section class='upload-info'>
<%= ctx.makeUserLink(ctx.post.user) %>,
<%= ctx.makeRelativeTime(ctx.post.creationTime) %>
</section>
<% if (ctx.enableSafety) { %>
<section class='safety'>
<i class='fa fa-circle safety-<%- ctx.post.safety %>'></i><!--
--><%- ctx.post.safety[0].toUpperCase() + ctx.post.safety.slice(1) %>
</section>
<% } %>
<section class='zoom'>
<a href class='fit-original'>Original zoom</a> &middot;
<a href class='fit-width'>fit width</a> &middot;
<a href class='fit-height'>height</a> &middot;
<a href class='fit-both'>both</a>
</section>
<% if (ctx.post.source) { %>
<section class='source'>
Source: <% for (let i = 0; i < ctx.post.sourceSplit.length; i++) { %>
<% if (i != 0) { %>&middot;<% } %>
<a href='<%- ctx.post.sourceSplit[i] %>' title='<%- ctx.post.sourceSplit[i] %>'><%- ctx.extractRootDomain(ctx.post.sourceSplit[i]) %></a>
<% } %>
</section>
<% } %>
<section class='search'>
Search on
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> &middot;
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> &middot;
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
</section>
<section class='social'>
<div class='score-container'></div>
<div class='fav-container'></div>
</section>
</article>
<% if (ctx.post.relations.length) { %>
<nav class='relations'>
<h1>Relations (<%- ctx.post.relations.length %>)</h1>
<ul><!--
--><% for (let post of ctx.post.relations) { %><!--
--><li><!--
--><a href='<%= ctx.getPostUrl(post.id, ctx.parameters) %>'><!--
--><%= ctx.makeThumbnail(post.thumbnailUrl) %><!--
--></a><!--
--></li><!--
--><% } %><!--
--></ul>
</nav>
<% } %>
<nav class='tags'>
<h1>Tags (<%- ctx.post.tags.length %>)</h1>
<% if (ctx.post.tags.length) { %>
<ul class='compact-tags'><!--
--><% for (let tag of ctx.post.tags) { %><!--
--><li><!--
--><% if (ctx.canViewTags) { %><!--
--><a href='<%- ctx.formatClientLink('tag', tag.names[0]) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
--><i class='fa fa-tag'></i><!--
--><% } %><!--
--><% if (ctx.canViewTags) { %><!--
--></a><!--
--><% } %><!--
--><% if (ctx.canListPosts) { %><!--
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
--><% } %><!--
--><%- ctx.getPrettyName(tag.names[0]) %><!--
--><% if (ctx.canListPosts) { %><!--
--></a><!--
--><% } %>&#32;<!--
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
--></li><!--
--><% } %><!--
--></ul>
<% } else { %>
<p>
No tags yet!
<% if (ctx.canEditPosts) { %>
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>Add some.</a>
<% } %>
</p>
<% } %>
</nav>
</div>

View file

@ -0,0 +1,39 @@
<div id='post-upload'>
<form>
<div class='dropper-container'></div>
<div class='control-strip'>
<input type='submit' value='Upload all' class='submit'/>
<span class='skip-duplicates'>
<%= ctx.makeCheckbox({
text: 'Skip duplicate',
name: 'skip-duplicates',
checked: false,
}) %>
</span>
<span class='always-upload-similar'>
<%= ctx.makeCheckbox({
text: 'Force upload similar',
name: 'always-upload-similar',
checked: false,
}) %>
</span>
<span class='pause-remain-on-error'>
<%= ctx.makeCheckbox({
text: 'Pause on error',
name: 'pause-remain-on-error',
checked: true,
}) %>
</span>
<input type='button' value='Cancel' class='cancel'/>
</div>
<div class='messages'></div>
<ul class='uploadables-container'></ul>
</form>
</div>

View file

@ -0,0 +1,96 @@
<li class='uploadable-container'>
<div class='thumbnail-wrapper'>
<% if (['image'].includes(ctx.uploadable.type)) { %>
<a href='<%= ctx.uploadable.previewUrl %>'>
<%= ctx.makeThumbnail(ctx.uploadable.previewUrl) %>
</a>
<% } else if (['video'].includes(ctx.uploadable.type)) { %>
<div class='thumbnail'>
<a href='<%= ctx.uploadable.previewUrl %>'>
<video id='video' nocontrols muted>
<source type='<%- ctx.uploadable.mimeType %>' src='<%- ctx.uploadable.previewUrl %>'/>
</video>
</a>
</div>
<% } else { %>
<%= ctx.makeThumbnail(null) %>
<% } %>
</div>
<div class='uploadable'>
<header>
<nav>
<ul>
<li><a href class='move-up'><i class='fa fa-chevron-up'></i></a></li>
<li><a href class='move-down'><i class='fa fa-chevron-down'></i></a></li>
</ul>
</nav>
<nav>
<ul>
<li><a href class='remove'><i class='fa fa-remove'></i></a></li>
</ul>
</nav>
<span class='filename'><%= ctx.uploadable.name %></span>
</header>
<div class='body'>
<% if (ctx.enableSafety) { %>
<div class='safety'>
<% for (let safety of ['safe', 'sketchy', 'unsafe']) { %>
<%= ctx.makeRadio({
name: 'safety-' + ctx.uploadable.key,
value: safety,
text: safety[0].toUpperCase() + safety.substr(1),
selectedValue: ctx.uploadable.safety,
}) %>
<% } %>
</div>
<% } %>
<div class='options'>
<% if (ctx.canUploadAnonymously) { %>
<div class='anonymous'>
<%= ctx.makeCheckbox({
text: 'Upload anonymously',
name: 'anonymous',
checked: ctx.uploadable.anonymous,
readonly: ctx.uploadable.forceAnonymous,
}) %>
</div>
<% } %>
</div>
<div class='messages'></div>
<% if (ctx.uploadable.lookalikes.length) { %>
<ul class='lookalikes'>
<% for (let lookalike of ctx.uploadable.lookalikes) { %>
<li>
<a class='thumbnail-wrapper' title='@<%- lookalike.post.id %>'
href='<%= ctx.canViewPosts ? ctx.getPostUrl(lookalike.post.id) : "" %>'>
<%= ctx.makeThumbnail(lookalike.post.thumbnailUrl) %>
</a>
<div class='description'>
Similar post: <%= ctx.makePostLink(lookalike.post.id, true) %>
<br/>
<%- Math.round((1-lookalike.distance) * 100) %>% match
</div>
<div class='controls'>
<%= ctx.makeCheckbox({text: 'Copy tags', name: 'copy-tags'}) %>
<br/>
<%= ctx.makeCheckbox({text: 'Add relation', name: 'add-relation'}) %>
</div>
</li>
<% } %>
</ul>
<% } %>
</div>
</div>
</li>

View file

@ -0,0 +1,37 @@
<div class='post-list-header'><%
%><form class='horizontal search'><%
%><%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %><%
%><wbr/><%
%><input class='mousetrap' type='submit' value='Search'/><%
%><wbr/><%
%><% if (ctx.enableSafety) { %><%
%><input data-safety=safe type='button' class='mousetrap safety safety-safe <%- ctx.settings.listPosts.safe ? '' : 'disabled' %>'/><%
%><input data-safety=sketchy type='button' class='mousetrap safety safety-sketchy <%- ctx.settings.listPosts.sketchy ? '' : 'disabled' %>'/><%
%><input data-safety=unsafe type='button' class='mousetrap safety safety-unsafe <%- ctx.settings.listPosts.unsafe ? '' : 'disabled' %>'/><%
%><% } %><%
%><wbr/><%
%><a class='mousetrap button append' href='<%- ctx.formatClientLink('help', 'search', 'posts') %>'>Syntax help</a><%
%></form><%
%><% if (ctx.canBulkEditTags) { %><%
%><form class='horizontal bulk-edit bulk-edit-tags'><%
%><span class='append hint'>Tagging with:</span><%
%><a href class='mousetrap button append open'>Mass tag</a><%
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
%><input class='mousetrap start' type='submit' value='Start tagging'/><%
%><a href class='mousetrap button append close'>Stop tagging</a><%
%></form><%
%><% } %><%
%><% if (ctx.enableSafety && ctx.canBulkEditSafety) { %><%
%><form class='horizontal bulk-edit bulk-edit-safety'><%
%><a href class='mousetrap button append open'>Mass edit safety</a><%
%><a href class='mousetrap button append close'>Stop editing safety</a><%
%></form><%
%><% } %><%
%><% if (ctx.canBulkDelete) { %><%
%><form class='horizontal bulk-edit bulk-edit-delete'><%
%><a href class='mousetrap button append open'>Mass delete</a><%
%><input class='mousetrap start' type='submit' value='Delete selected posts'/><%
%><a href class='mousetrap button append close'>Stop deleting</a><%
%></form><%
%><% } %><%
%></div>

View file

@ -0,0 +1,63 @@
<% if (ctx.postFlow) { %><div class='post-list post-flow'><% } else { %><div class='post-list'><% } %>
<% if (ctx.response.results.length) { %>
<ul>
<% for (let post of ctx.response.results) { %>
<li data-post-id='<%= post.id %>'>
<a class='thumbnail-wrapper <%= post.tags.length > 0 ? "tags" : "no-tags" %>'
title='@<%- post.id %> (<%- post.type %>)&#10;&#10;Tags: <%- post.tags.map(tag => '#' + tag.names[0]).join(' ') || 'none' %>'
href='<%= ctx.canViewPosts ? ctx.getPostUrl(post.id, ctx.parameters) : '' %>'>
<%= ctx.makeThumbnail(post.thumbnailUrl) %>
<span class='type' data-type='<%- post.type %>'>
<% if (post.type == 'video' || post.type == 'flash' || post.type == 'animation') { %>
<span class='icon'><i class='fa fa-film'></i></span>
<% } else { %>
<%- post.type %>
<% } %>
</span>
<% if (post.score || post.favoriteCount || post.commentCount) { %>
<span class='stats'>
<% if (post.score) { %>
<span class='icon'>
<i class='fa fa-thumbs-up'></i>
<%- post.score %>
</span>
<% } %>
<% if (post.favoriteCount) { %>
<span class='icon'>
<i class='fa fa-heart'></i>
<%- post.favoriteCount %>
</span>
<% } %>
<% if (post.commentCount) { %>
<span class='icon'>
<i class='fa fa-commenting'></i>
<%- post.commentCount %>
</span>
<% } %>
</span>
<% } %>
</a>
<span class='edit-overlay'>
<% if (ctx.canBulkEditTags && ctx.parameters && ctx.parameters.tag) { %>
<a href class='tag-flipper'>
</a>
<% } %>
<% if (ctx.canBulkEditSafety && ctx.parameters && ctx.parameters.safety) { %>
<span class='safety-flipper'>
<% for (let safety of ['safe', 'sketchy', 'unsafe']) { %>
<a href data-safety='<%- safety %>' class='safety-<%- safety %><%- post.safety === safety ? ' active' : '' %>'>
</a>
<% } %>
</span>
<% } %>
<% if (ctx.canBulkDelete && ctx.parameters && ctx.parameters.delete) { %>
<a href class='delete-flipper'>
</a>
<% } %>
</span>
</li>
<% } %>
<%= ctx.makeFlexboxAlign() %>
</ul>
<% } %>
</div>

Some files were not shown because too many files have changed in this diff Show more