init
This commit is contained in:
commit
2e3e7097e6
108 changed files with 21946 additions and 0 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.stack
|
||||||
|
.stack-work
|
3
.env
Normal file
3
.env
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
HUB_REPO=localhost
|
||||||
|
APPIMAGE=espial:espial
|
||||||
|
APPDATA=.
|
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
dist*
|
||||||
|
static/tmp/
|
||||||
|
static/combined/
|
||||||
|
config/client_session_key.aes
|
||||||
|
*.hi
|
||||||
|
*.o
|
||||||
|
*.sqlite3
|
||||||
|
*.sqlite3-shm
|
||||||
|
*.sqlite3-wal
|
||||||
|
.hsenv*
|
||||||
|
cabal-dev/
|
||||||
|
.stack-work/
|
||||||
|
yesod-devel/
|
||||||
|
.cabal-sandbox
|
||||||
|
cabal.sandbox.config
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*.keter
|
||||||
|
*~
|
||||||
|
\#*
|
||||||
|
\.#*
|
||||||
|
test-project.cabal
|
||||||
|
bookmarks-*.json
|
||||||
|
TAGS
|
||||||
|
purs/dist
|
||||||
|
purs/output
|
||||||
|
purs/generated-docs
|
||||||
|
tmp
|
||||||
|
.cache
|
||||||
|
tags
|
6
Dockerfile
Normal file
6
Dockerfile
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#-*- mode:conf; -*-
|
||||||
|
|
||||||
|
FROM jonschoning/espial:scratch
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
CMD ./espial +RTS -T
|
660
LICENSE
Normal file
660
LICENSE
Normal file
|
@ -0,0 +1,660 @@
|
||||||
|
### GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc.
|
||||||
|
<https://fsf.org/>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this
|
||||||
|
license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
### Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing
|
||||||
|
under this license.
|
||||||
|
|
||||||
|
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your
|
||||||
|
version supports such interaction) an opportunity to receive the
|
||||||
|
Corresponding Source of your version by providing access to the
|
||||||
|
Corresponding Source from a network server at no charge, through some
|
||||||
|
standard or customary means of facilitating copying of software. This
|
||||||
|
Corresponding Source shall include the Corresponding Source for any
|
||||||
|
work covered by version 3 of the GNU General Public License that is
|
||||||
|
incorporated pursuant to the following paragraph.
|
||||||
|
|
||||||
|
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 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 work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
#### 14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper
|
||||||
|
mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for
|
||||||
|
the specific requirements.
|
||||||
|
|
||||||
|
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 AGPL, see <https://www.gnu.org/licenses/>.
|
67
Makefile
Normal file
67
Makefile
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
.PHONY: clean build
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
@stack build
|
||||||
|
|
||||||
|
build-fast:
|
||||||
|
@stack build --fast
|
||||||
|
|
||||||
|
build-watch:
|
||||||
|
@stack build --file-watch --fast --ghc-options=-fno-code
|
||||||
|
|
||||||
|
repl:
|
||||||
|
@stack ghci --test --bench --ghci-options=-fno-code --main-is=espial:exe:espial
|
||||||
|
|
||||||
|
ghcid:
|
||||||
|
@ghcid -c "stack ghci --test --bench --ghci-options=-fno-code --main-is=espial:exe:espial"
|
||||||
|
|
||||||
|
devel:
|
||||||
|
@yesod devel
|
||||||
|
|
||||||
|
migrate-createdb:
|
||||||
|
@stack exec migration -- createdb --conn espial.sqlite3
|
||||||
|
|
||||||
|
serve:
|
||||||
|
@stack exec espial -- +RTS -T
|
||||||
|
|
||||||
|
_ESPIAL_PS_ID = $$(docker-compose ps -q espial)
|
||||||
|
_LOCAL_INSTALL_PATH = $$(stack path | grep local-install-root | awk -e '{print $$2}')
|
||||||
|
_EKG_ASSETS_PATH = $$(find .stack-work -type d | grep ekg.*assets)
|
||||||
|
|
||||||
|
docker-compose-build: build
|
||||||
|
@rm -Rf dist && mkdir -p dist
|
||||||
|
@cp $(_LOCAL_INSTALL_PATH)/bin/* dist
|
||||||
|
@cp -R static dist
|
||||||
|
@rm -Rf dist/static/tmp
|
||||||
|
@cp -R config dist
|
||||||
|
@mkdir -p dist/ekg/assets
|
||||||
|
@cp -R $(_EKG_ASSETS_PATH) dist/ekg
|
||||||
|
@docker-compose build espial
|
||||||
|
docker-compose-up:
|
||||||
|
@docker-compose up --no-deps --no-build espial
|
||||||
|
docker-compose-down:
|
||||||
|
@docker-compose down
|
||||||
|
docker-compose-up-d:
|
||||||
|
@docker-compose up --no-deps --no-build -d espial
|
||||||
|
docker-compose-pull:
|
||||||
|
@docker-compose pull espial
|
||||||
|
docker-compose-push:
|
||||||
|
@docker tag localhost/espial:espial $(HUB_REPO)/espial:espial
|
||||||
|
@docker-compose push espial
|
||||||
|
docker-espial-logs:
|
||||||
|
@docker logs -f --since `date -u +%FT%TZ` $(_ESPIAL_PS_ID)
|
||||||
|
docker-espial-shell:
|
||||||
|
@$(docker_espial) sh
|
||||||
|
|
||||||
|
|
||||||
|
_HUB_REPO = ${HUB_REPO}
|
||||||
|
ifeq ($(_HUB_REPO),)
|
||||||
|
_HUB_REPO := "localhost"
|
||||||
|
endif
|
||||||
|
|
||||||
|
docker_espial = docker-compose exec espial
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@stack clean
|
89
README.md
Normal file
89
README.md
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# Espial
|
||||||
|
|
||||||
|
Espial is an open-source, web-based bookmarking server.
|
||||||
|
|
||||||
|
It allows mutiple accounts, but currently intended for self-host scenarios.
|
||||||
|
|
||||||
|
The bookmarks are stored in a sqlite3 database, for ease of deployment & maintenence.
|
||||||
|
|
||||||
|
The easist way for logged-in users to add bookmarks, is with the "bookmarklet", found on the Settings page.
|
||||||
|
|
||||||
|
## demo server
|
||||||
|
|
||||||
|
log in — username: demo password: demo
|
||||||
|
|
||||||
|
https://esp.ae8.org/u:demo
|
||||||
|
|
||||||
|
![jpg](https://i.imgur.com/2viEMQj.png)
|
||||||
|
|
||||||
|
## Server Setup (from source)
|
||||||
|
|
||||||
|
1. [Install Stack](https://haskell-lang.org/get-started)
|
||||||
|
- On POSIX systems, this is usually `curl -sSL https://get.haskellstack.org/ | sh`
|
||||||
|
|
||||||
|
2. Build executables
|
||||||
|
|
||||||
|
```
|
||||||
|
stack build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create the database
|
||||||
|
|
||||||
|
```
|
||||||
|
stack exec migration -- createdb --conn espial.sqlite3
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create a user
|
||||||
|
|
||||||
|
```
|
||||||
|
stack exec migration -- createuser --conn espial.sqlite3 --userName myusername --userPassword myuserpassword
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Import a bookmark file for a user (optional)
|
||||||
|
|
||||||
|
```
|
||||||
|
stack exec migration -- importbookmarks --conn espial.sqlite3 --userName myusername --bookmarkFile sample-bookmarks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Start a production server:
|
||||||
|
|
||||||
|
```
|
||||||
|
stack exec espial -- +RTS -T
|
||||||
|
```
|
||||||
|
|
||||||
|
see `config/settings.yml` for changing default run-time parameters / environment variables
|
||||||
|
|
||||||
|
default app http port: `3000`
|
||||||
|
|
||||||
|
default ekg http port: `8000`
|
||||||
|
|
||||||
|
ssl: use reverse proxy
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
- Install the `yesod` command line tool: `stack install yesod-bin --install-ghc`
|
||||||
|
|
||||||
|
|
||||||
|
- Start a development server:
|
||||||
|
|
||||||
|
```
|
||||||
|
yesod devel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- See `purs/` folder
|
||||||
|
|
||||||
|
## Import Bookmark file format
|
||||||
|
|
||||||
|
see `sample-bookmarks.json`, which contains a JSON array, each line containing a `FileBookmark` object.
|
||||||
|
|
||||||
|
example:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ {"href":"http://raganwald.com/2018/02/23/forde.html","description":"Forde's Tenth Rule, or, \"How I Learned to Stop Worrying and \u2764\ufe0f the State Machine\"","extended":"","time":"2018-02-26T22:57:20Z","shared":"yes","toread":"yes","tags":"raganwald"},
|
||||||
|
, {"href":"http://downloads.haskell.org/~ghc/latest/docs/html/users_guide/flags.html","description":"7.6. Flag reference \u2014 Glasgow Haskell Compiler 8.2.2 User's Guide","extended":"-fprint-expanded-synonyms","time":"2018-02-26T21:52:02Z","shared":"yes","toread":"no","tags":"ghc haskell"},
|
||||||
|
]
|
||||||
|
```
|
99
app/DevelMain.hs
Normal file
99
app/DevelMain.hs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
-- | Running your app inside GHCi.
|
||||||
|
--
|
||||||
|
-- To start up GHCi for usage with Yesod, first make sure you are in dev mode:
|
||||||
|
--
|
||||||
|
-- > cabal configure -fdev
|
||||||
|
--
|
||||||
|
-- Note that @yesod devel@ automatically sets the dev flag.
|
||||||
|
-- Now launch the repl:
|
||||||
|
--
|
||||||
|
-- > cabal repl --ghc-options="-O0 -fobject-code"
|
||||||
|
--
|
||||||
|
-- To start your app, run:
|
||||||
|
--
|
||||||
|
-- > :l DevelMain
|
||||||
|
-- > DevelMain.update
|
||||||
|
--
|
||||||
|
-- You can also call @DevelMain.shutdown@ to stop the app
|
||||||
|
--
|
||||||
|
-- You will need to add the foreign-store package to your .cabal file.
|
||||||
|
-- It is very light-weight.
|
||||||
|
--
|
||||||
|
-- If you don't use cabal repl, you will need
|
||||||
|
-- to run the following in GHCi or to add it to
|
||||||
|
-- your .ghci file.
|
||||||
|
--
|
||||||
|
-- :set -DDEVELOPMENT
|
||||||
|
--
|
||||||
|
-- There is more information about this approach,
|
||||||
|
-- on the wiki: https://github.com/yesodweb/yesod/wiki/ghci
|
||||||
|
|
||||||
|
module DevelMain where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
import Application (getApplicationRepl, shutdownApp)
|
||||||
|
|
||||||
|
import Control.Exception (finally)
|
||||||
|
import Control.Monad ((>=>))
|
||||||
|
import Control.Concurrent
|
||||||
|
import Data.IORef
|
||||||
|
import Foreign.Store
|
||||||
|
import Network.Wai.Handler.Warp
|
||||||
|
import GHC.Word
|
||||||
|
|
||||||
|
-- | Start or restart the server.
|
||||||
|
-- newStore is from foreign-store.
|
||||||
|
-- A Store holds onto some data across ghci reloads
|
||||||
|
update :: IO ()
|
||||||
|
update = do
|
||||||
|
mtidStore <- lookupStore tidStoreNum
|
||||||
|
case mtidStore of
|
||||||
|
-- no server running
|
||||||
|
Nothing -> do
|
||||||
|
done <- storeAction doneStore newEmptyMVar
|
||||||
|
tid <- start done
|
||||||
|
_ <- storeAction (Store tidStoreNum) (newIORef tid)
|
||||||
|
return ()
|
||||||
|
-- server is already running
|
||||||
|
Just tidStore -> restartAppInNewThread tidStore
|
||||||
|
where
|
||||||
|
doneStore :: Store (MVar ())
|
||||||
|
doneStore = Store 0
|
||||||
|
|
||||||
|
-- shut the server down with killThread and wait for the done signal
|
||||||
|
restartAppInNewThread :: Store (IORef ThreadId) -> IO ()
|
||||||
|
restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do
|
||||||
|
killThread tid
|
||||||
|
withStore doneStore takeMVar
|
||||||
|
readStore doneStore >>= start
|
||||||
|
|
||||||
|
|
||||||
|
-- | Start the server in a separate thread.
|
||||||
|
start :: MVar () -- ^ Written to when the thread is killed.
|
||||||
|
-> IO ThreadId
|
||||||
|
start done = do
|
||||||
|
(port, site, app) <- getApplicationRepl
|
||||||
|
forkIO (finally (runSettings (setPort port defaultSettings) app)
|
||||||
|
-- Note that this implies concurrency
|
||||||
|
-- between shutdownApp and the next app that is starting.
|
||||||
|
-- Normally this should be fine
|
||||||
|
(putMVar done () >> shutdownApp site))
|
||||||
|
|
||||||
|
-- | kill the server
|
||||||
|
shutdown :: IO ()
|
||||||
|
shutdown = do
|
||||||
|
mtidStore <- lookupStore tidStoreNum
|
||||||
|
case mtidStore of
|
||||||
|
-- no server running
|
||||||
|
Nothing -> putStrLn "no Yesod app running"
|
||||||
|
Just tidStore -> do
|
||||||
|
withStore tidStore $ readIORef >=> killThread
|
||||||
|
putStrLn "Yesod app is shutdown"
|
||||||
|
|
||||||
|
tidStoreNum :: Word32
|
||||||
|
tidStoreNum = 1
|
||||||
|
|
||||||
|
modifyStoredIORef :: Store (IORef a) -> (a -> IO a) -> IO ()
|
||||||
|
modifyStoredIORef store f = withStore store $ \ref -> do
|
||||||
|
v <- readIORef ref
|
||||||
|
f v >>= writeIORef ref
|
6
app/devel.hs
Normal file
6
app/devel.hs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{-# LANGUAGE PackageImports #-}
|
||||||
|
import "espial" Application (develMain)
|
||||||
|
import Prelude (IO)
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = develMain
|
5
app/main.hs
Normal file
5
app/main.hs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import Prelude (IO)
|
||||||
|
import Application (appMain)
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = appMain
|
83
app/migration/Main.hs
Normal file
83
app/migration/Main.hs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
|
||||||
|
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Types
|
||||||
|
import Model
|
||||||
|
import ModelCustom
|
||||||
|
|
||||||
|
import qualified Database.Persist as P
|
||||||
|
import qualified Database.Persist.Sqlite as P
|
||||||
|
|
||||||
|
import ClassyPrelude
|
||||||
|
import Lens.Micro
|
||||||
|
|
||||||
|
import Options.Generic
|
||||||
|
|
||||||
|
data MigrationOpts
|
||||||
|
= CreateDB { conn :: Text}
|
||||||
|
| CreateUser { conn :: Text
|
||||||
|
, userName :: Text
|
||||||
|
, userPassword :: Text
|
||||||
|
, userApiToken :: Maybe Text }
|
||||||
|
| DeleteUser { conn :: Text
|
||||||
|
, userName :: Text}
|
||||||
|
| ImportBookmarks { conn :: Text
|
||||||
|
, userName :: Text
|
||||||
|
, bookmarkFile :: FilePath}
|
||||||
|
| ImportNotes { conn :: Text
|
||||||
|
, userName :: Text
|
||||||
|
, noteDirectory :: FilePath}
|
||||||
|
| PrintMigrateDB { conn :: Text}
|
||||||
|
deriving (Generic, Show)
|
||||||
|
|
||||||
|
instance ParseRecord MigrationOpts
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
args <- getRecord "Migrations"
|
||||||
|
case args of
|
||||||
|
PrintMigrateDB conn ->
|
||||||
|
P.runSqlite conn dumpMigration
|
||||||
|
|
||||||
|
CreateDB conn -> do
|
||||||
|
let connInfo = P.mkSqliteConnectionInfo conn
|
||||||
|
& set P.fkEnabled False
|
||||||
|
P.runSqliteInfo connInfo runMigrations
|
||||||
|
|
||||||
|
CreateUser conn uname upass utoken ->
|
||||||
|
P.runSqlite conn $ do
|
||||||
|
hash' <- liftIO (hashPassword upass)
|
||||||
|
void $ P.upsertBy
|
||||||
|
(UniqueUserName uname)
|
||||||
|
(User uname hash' utoken False False False)
|
||||||
|
[ UserPasswordHash P.=. hash'
|
||||||
|
, UserApiToken P.=. utoken
|
||||||
|
, UserPrivateDefault P.=. False
|
||||||
|
, UserArchiveDefault P.=. False
|
||||||
|
, UserPrivacyLock P.=. False
|
||||||
|
]
|
||||||
|
pure () :: DB ()
|
||||||
|
|
||||||
|
DeleteUser conn uname ->
|
||||||
|
P.runSqlite conn $ do
|
||||||
|
muser <- P.getBy (UniqueUserName uname)
|
||||||
|
case muser of
|
||||||
|
Nothing -> liftIO (print (uname ++ "not found"))
|
||||||
|
Just (P.Entity uid _) -> do
|
||||||
|
P.deleteCascade uid
|
||||||
|
pure () :: DB ()
|
||||||
|
|
||||||
|
ImportBookmarks conn uname file ->
|
||||||
|
P.runSqlite conn $ do
|
||||||
|
muser <- P.getBy (UniqueUserName uname)
|
||||||
|
case muser of
|
||||||
|
Just (P.Entity uid _) -> insertFileBookmarks uid file
|
||||||
|
Nothing -> liftIO (print (uname ++ "not found"))
|
||||||
|
|
||||||
|
ImportNotes conn uname dir ->
|
||||||
|
P.runSqlite conn $ do
|
||||||
|
muser <- P.getBy (UniqueUserName uname)
|
||||||
|
case muser of
|
||||||
|
Just (P.Entity uid _) -> insertDirFileNotes uid dir
|
||||||
|
Nothing -> liftIO (print (uname ++ "not found"))
|
4
changelog.md
Normal file
4
changelog.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
__v0.0.7__
|
||||||
|
|
||||||
|
init
|
BIN
config/favicon.ico
Normal file
BIN
config/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 198 B |
70
config/keter.yml
Normal file
70
config/keter.yml
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# After you've edited this file, remove the following line to allow
|
||||||
|
# `yesod keter` to build your bundle.
|
||||||
|
user-edited: false
|
||||||
|
|
||||||
|
# A Keter app is composed of 1 or more stanzas. The main stanza will define our
|
||||||
|
# web application. See the Keter documentation for more information on
|
||||||
|
# available stanzas.
|
||||||
|
stanzas:
|
||||||
|
|
||||||
|
# Your Yesod application.
|
||||||
|
- type: webapp
|
||||||
|
|
||||||
|
# Name of your executable. You are unlikely to need to change this.
|
||||||
|
# Note that all file paths are relative to the keter.yml file.
|
||||||
|
#
|
||||||
|
# The path given is for Stack projects. If you're still using cabal, change
|
||||||
|
# to
|
||||||
|
# exec: ../dist/build/espial/espial
|
||||||
|
exec: ../dist/bin/espial
|
||||||
|
|
||||||
|
# Command line options passed to your application.
|
||||||
|
args: []
|
||||||
|
|
||||||
|
hosts:
|
||||||
|
# You can specify one or more hostnames for your application to respond
|
||||||
|
# to. The primary hostname will be used for generating your application
|
||||||
|
# root.
|
||||||
|
- www.espial.com
|
||||||
|
|
||||||
|
# Enable to force Keter to redirect to https
|
||||||
|
# Can be added to any stanza
|
||||||
|
requires-secure: false
|
||||||
|
|
||||||
|
# Static files.
|
||||||
|
- type: static-files
|
||||||
|
hosts:
|
||||||
|
- static.espial.com
|
||||||
|
root: ../static
|
||||||
|
|
||||||
|
# Uncomment to turn on directory listings.
|
||||||
|
# directory-listing: true
|
||||||
|
|
||||||
|
# Redirect plain domain name to www.
|
||||||
|
- type: redirect
|
||||||
|
|
||||||
|
hosts:
|
||||||
|
- espial.com
|
||||||
|
actions:
|
||||||
|
- host: www.espial.com
|
||||||
|
# secure: false
|
||||||
|
# port: 80
|
||||||
|
|
||||||
|
# Uncomment to switch to a non-permanent redirect.
|
||||||
|
# status: 303
|
||||||
|
|
||||||
|
# Use the following to automatically copy your bundle upon creation via `yesod
|
||||||
|
# keter`. Uses `scp` internally, so you can set it to a remote destination
|
||||||
|
# copy-to: user@host:/opt/keter/incoming/
|
||||||
|
|
||||||
|
# You can pass arguments to `scp` used above. This example limits bandwidth to
|
||||||
|
# 1024 Kbit/s and uses port 2222 instead of the default 22
|
||||||
|
# copy-to-args:
|
||||||
|
# - "-l 1024"
|
||||||
|
# - "-P 2222"
|
||||||
|
|
||||||
|
# If you would like to have Keter automatically create a PostgreSQL database
|
||||||
|
# and set appropriate environment variables for it to be discovered, uncomment
|
||||||
|
# the following line.
|
||||||
|
# plugins:
|
||||||
|
# postgres: true
|
1
config/robots.txt
Normal file
1
config/robots.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
User-agent: *
|
40
config/routes
Normal file
40
config/routes
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/static StaticR Static appStatic
|
||||||
|
|
||||||
|
/favicon.ico FaviconR GET
|
||||||
|
/robots.txt RobotsR GET
|
||||||
|
|
||||||
|
/auth AuthR Auth getAuth
|
||||||
|
|
||||||
|
-- notes
|
||||||
|
!/#UserNameP/notes NotesR GET
|
||||||
|
!/#UserNameP/notes/add AddNoteViewR GET
|
||||||
|
!/#UserNameP/notes/#NtSlug NoteR GET
|
||||||
|
!/api/note/add AddNoteR POST
|
||||||
|
!/api/note/#Int64 DeleteNoteR DELETE
|
||||||
|
|
||||||
|
-- user
|
||||||
|
/ HomeR GET
|
||||||
|
!/#UserNameP UserR GET
|
||||||
|
!/#UserNameP/#SharedP UserSharedR GET
|
||||||
|
!/#UserNameP/#FilterP UserFilterR GET
|
||||||
|
!/#UserNameP/#TagsP UserTagsR GET
|
||||||
|
|
||||||
|
-- settings
|
||||||
|
/Settings AccountSettingsR GET
|
||||||
|
api/accountSettings EditAccountSettingsR POST
|
||||||
|
|
||||||
|
-- settings/password
|
||||||
|
/Settings/Password ChangePasswordR GET POST
|
||||||
|
|
||||||
|
-- add
|
||||||
|
/add AddViewR GET
|
||||||
|
api/add AddR POST
|
||||||
|
|
||||||
|
-- edit
|
||||||
|
/bm/#Int64 DeleteR DELETE
|
||||||
|
/bm/#Int64/read ReadR POST
|
||||||
|
/bm/#Int64/star StarR POST
|
||||||
|
/bm/#Int64/unstar UnstarR POST
|
||||||
|
|
||||||
|
-- doc
|
||||||
|
/docs/search DocsSearchR GET
|
41
config/settings.yml
Normal file
41
config/settings.yml
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Values formatted like "_env:ENV_VAR_NAME:default_value" can be overridden by the specified environment variable.
|
||||||
|
# See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables
|
||||||
|
|
||||||
|
static-dir: "_env:STATIC_DIR:static"
|
||||||
|
host: "_env:HOST:*4" # any IPv4 host
|
||||||
|
port: "_env:PORT:3000" # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line.
|
||||||
|
ip-from-header: "_env:IP_FROM_HEADER:false"
|
||||||
|
|
||||||
|
# Default behavior: determine the application root from the request headers.
|
||||||
|
# Uncomment to set an explicit approot
|
||||||
|
#approot: "_env:APPROOT:http://localhost:3000"
|
||||||
|
|
||||||
|
# By default, `yesod devel` runs in development, and built executables use
|
||||||
|
# production settings (see below). To override this, use the following:
|
||||||
|
#
|
||||||
|
# development: false
|
||||||
|
|
||||||
|
# Optional values with the following production defaults.
|
||||||
|
# In development, they default to the inverse.
|
||||||
|
#
|
||||||
|
# detailed-logging: false
|
||||||
|
# should-log-all: false
|
||||||
|
# reload-templates: false
|
||||||
|
# mutable-static: false
|
||||||
|
# skip-combining: false
|
||||||
|
# auth-dummy-login : false
|
||||||
|
|
||||||
|
# NB: If you need a numeric value (e.g. 123) to parse as a String, wrap it in single quotes (e.g. "_env:PGPASS:'123'")
|
||||||
|
# See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings
|
||||||
|
|
||||||
|
database:
|
||||||
|
# See config/test-settings.yml for an override during tests
|
||||||
|
database: "_env:SQLITE_DATABASE:espial.sqlite3"
|
||||||
|
# database: ":memory:"
|
||||||
|
poolsize: "_env:SQLITE_POOLSIZE:10"
|
||||||
|
|
||||||
|
copyright: Insert copyright statement here
|
||||||
|
#analytics: UA-YOURCODE
|
||||||
|
|
||||||
|
ekg-host: "_env:EKG_HOST:0.0.0.0"
|
||||||
|
ekg-port: "_env:EKG_PORT:8000"
|
12
config/test-settings.yml
Normal file
12
config/test-settings.yml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
database:
|
||||||
|
# NOTE: By design, this setting prevents the SQLITE_DATABASE environment variable
|
||||||
|
# from affecting test runs, so that we don't accidentally affect the
|
||||||
|
# production database during testing. If you're not concerned about that and
|
||||||
|
# would like to have environment variable overrides, you could instead use
|
||||||
|
# something like:
|
||||||
|
#
|
||||||
|
# database: "_env:SQLITE_DATABASE:espial_test.sqlite3"
|
||||||
|
# database: espial_test.sqlite3
|
||||||
|
database: ":memory:"
|
||||||
|
|
||||||
|
auth-dummy-login: true
|
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
espial:
|
||||||
|
image: '$HUB_REPO/$APPIMAGE'
|
||||||
|
build:
|
||||||
|
context: dist
|
||||||
|
dockerfile: ../Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- '$APPDATA:/app/data'
|
||||||
|
environment:
|
||||||
|
- IP_FROM_HEADER=true
|
||||||
|
- SQLITE_DATABASE=/app/data/espial.sqlite3
|
||||||
|
- ekg_datadir=ekg
|
406
espial.cabal
Normal file
406
espial.cabal
Normal file
|
@ -0,0 +1,406 @@
|
||||||
|
-- This file has been generated from package.yaml by hpack version 0.28.2.
|
||||||
|
--
|
||||||
|
-- see: https://github.com/sol/hpack
|
||||||
|
--
|
||||||
|
-- hash: 417de4bead54d60a2c091ad91c61dc715571ef7421e702f157a3766daf4f4700
|
||||||
|
|
||||||
|
name: espial
|
||||||
|
version: 0.0.7
|
||||||
|
synopsis: Espial is an open-source, web-based bookmarking server.
|
||||||
|
description: .
|
||||||
|
Espial is an open-source, web-based bookmarking server.
|
||||||
|
- Yesod + PureScript + sqlite3
|
||||||
|
- multi-user (w/ privacy scopes)
|
||||||
|
- tags, stars, editing, deleting
|
||||||
|
category: Web
|
||||||
|
homepage: https://github.com/jonschoning/espial
|
||||||
|
bug-reports: https://github.com/jonschoning/espial/issues
|
||||||
|
author: Jon Schoning
|
||||||
|
maintainer: jonschoning@gmail.com
|
||||||
|
copyright: Copyright (c) 2018 Jon Schoning
|
||||||
|
license: AGPL-3
|
||||||
|
license-file: LICENSE
|
||||||
|
build-type: Simple
|
||||||
|
cabal-version: >= 1.10
|
||||||
|
extra-source-files:
|
||||||
|
changelog.md
|
||||||
|
config/favicon.ico
|
||||||
|
config/keter.yml
|
||||||
|
config/robots.txt
|
||||||
|
config/routes
|
||||||
|
config/settings.yml
|
||||||
|
config/test-settings.yml
|
||||||
|
purs/Makefile
|
||||||
|
purs/packages.dhall
|
||||||
|
purs/spago.dhall
|
||||||
|
purs/src/App.purs
|
||||||
|
purs/src/Component/AccountSettings.purs
|
||||||
|
purs/src/Component/Add.purs
|
||||||
|
purs/src/Component/BList.purs
|
||||||
|
purs/src/Component/BMark.purs
|
||||||
|
purs/src/Component/Markdown.purs
|
||||||
|
purs/src/Component/NList.purs
|
||||||
|
purs/src/Component/NNote.purs
|
||||||
|
purs/src/Component/RawHtml.js
|
||||||
|
purs/src/Component/RawHtml.purs
|
||||||
|
purs/src/Globals.js
|
||||||
|
purs/src/Globals.purs
|
||||||
|
purs/src/Main.purs
|
||||||
|
purs/src/Marked.js
|
||||||
|
purs/src/Marked.purs
|
||||||
|
purs/src/Model.purs
|
||||||
|
purs/src/Util.purs
|
||||||
|
purs/test/Main.purs
|
||||||
|
README.md
|
||||||
|
static/css/main.css
|
||||||
|
static/css/popup.css
|
||||||
|
static/css/tachyons.min.css
|
||||||
|
static/css/tachyons.min.css.gz
|
||||||
|
static/images/bluepin.gif
|
||||||
|
static/js/app.js
|
||||||
|
static/js/app.js.gz
|
||||||
|
static/js/app.min.js
|
||||||
|
static/js/app.min.js.gz
|
||||||
|
static/js/html5shiv.min.js
|
||||||
|
static/js/html5shiv.min.js.gz
|
||||||
|
static/js/js.cookie-2.2.0.min.js
|
||||||
|
static/js/js.cookie-2.2.0.min.js.gz
|
||||||
|
static/js/marked.min.js
|
||||||
|
static/js/marked.min.js.gz
|
||||||
|
static/js/moment.min.js
|
||||||
|
static/js/moment.min.js.gz
|
||||||
|
templates/change-password.hamlet
|
||||||
|
templates/default-layout-wrapper.hamlet
|
||||||
|
templates/default-layout.hamlet
|
||||||
|
templates/docs-search.hamlet
|
||||||
|
templates/homepage.hamlet
|
||||||
|
templates/login.hamlet
|
||||||
|
templates/note.hamlet
|
||||||
|
templates/notes.hamlet
|
||||||
|
templates/pager.hamlet
|
||||||
|
templates/popup-layout.hamlet
|
||||||
|
templates/search.hamlet
|
||||||
|
templates/user-settings.hamlet
|
||||||
|
templates/user.hamlet
|
||||||
|
|
||||||
|
source-repository head
|
||||||
|
type: git
|
||||||
|
location: git://github.com/jonschoning/espial.git
|
||||||
|
|
||||||
|
flag dev
|
||||||
|
description: Turn on development settings, like auto-reload templates.
|
||||||
|
manual: False
|
||||||
|
default: False
|
||||||
|
|
||||||
|
flag library-only
|
||||||
|
description: Build for use with "yesod devel"
|
||||||
|
manual: False
|
||||||
|
default: False
|
||||||
|
|
||||||
|
library
|
||||||
|
hs-source-dirs:
|
||||||
|
src
|
||||||
|
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
|
||||||
|
build-depends:
|
||||||
|
aeson >=1.4
|
||||||
|
, attoparsec
|
||||||
|
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
|
||||||
|
, bcrypt >=0.0.8
|
||||||
|
, bytestring >=0.9 && <0.11
|
||||||
|
, case-insensitive
|
||||||
|
, classy-prelude >=1.4 && <1.6
|
||||||
|
, classy-prelude-conduit >=1.4 && <1.6
|
||||||
|
, classy-prelude-yesod >=1.4 && <1.6
|
||||||
|
, conduit >=1.0 && <2.0
|
||||||
|
, containers
|
||||||
|
, data-default
|
||||||
|
, directory >=1.1 && <1.4
|
||||||
|
, ekg
|
||||||
|
, ekg-core
|
||||||
|
, entropy
|
||||||
|
, esqueleto
|
||||||
|
, fast-logger >=2.2 && <2.5
|
||||||
|
, file-embed
|
||||||
|
, foreign-store
|
||||||
|
, hjsmin >=0.1 && <0.3
|
||||||
|
, hscolour
|
||||||
|
, http-api-data >=0.3.4
|
||||||
|
, http-client
|
||||||
|
, http-client-tls >=0.3 && <0.4
|
||||||
|
, http-conduit >=2.3 && <2.4
|
||||||
|
, http-types
|
||||||
|
, iso8601-time >=0.1.3
|
||||||
|
, microlens
|
||||||
|
, monad-logger >=0.3 && <0.4
|
||||||
|
, monad-metrics
|
||||||
|
, mtl
|
||||||
|
, parser-combinators
|
||||||
|
, persistent >=2.8 && <2.10
|
||||||
|
, persistent-sqlite >=2.6.2
|
||||||
|
, persistent-template >=2.5 && <2.9
|
||||||
|
, pretty-show
|
||||||
|
, safe
|
||||||
|
, shakespeare >=2.0 && <2.1
|
||||||
|
, template-haskell
|
||||||
|
, text >=0.11 && <2.0
|
||||||
|
, time
|
||||||
|
, transformers >=0.2.2
|
||||||
|
, unordered-containers
|
||||||
|
, vector
|
||||||
|
, wai
|
||||||
|
, wai-extra >=3.0 && <3.1
|
||||||
|
, wai-logger >=2.2 && <2.4
|
||||||
|
, wai-middleware-metrics
|
||||||
|
, warp >=3.0 && <3.3
|
||||||
|
, yaml >=0.8 && <0.12
|
||||||
|
, yesod >=1.6 && <1.7
|
||||||
|
, yesod-auth >=1.6 && <1.7
|
||||||
|
, yesod-core >=1.6 && <1.7
|
||||||
|
, yesod-form >=1.6 && <1.7
|
||||||
|
, yesod-static >=1.6 && <1.7
|
||||||
|
if (flag(dev)) || (flag(library-only))
|
||||||
|
ghc-options: -Wall -fwarn-tabs -O0
|
||||||
|
cpp-options: -DDEVELOPMENT
|
||||||
|
else
|
||||||
|
ghc-options: -Wall -fwarn-tabs -O2
|
||||||
|
exposed-modules:
|
||||||
|
Application
|
||||||
|
Foundation
|
||||||
|
Generic
|
||||||
|
Handler.AccountSettings
|
||||||
|
Handler.Add
|
||||||
|
Handler.Archive
|
||||||
|
Handler.Common
|
||||||
|
Handler.Docs
|
||||||
|
Handler.Edit
|
||||||
|
Handler.Home
|
||||||
|
Handler.Notes
|
||||||
|
Handler.User
|
||||||
|
Import
|
||||||
|
Import.NoFoundation
|
||||||
|
Model
|
||||||
|
ModelCustom
|
||||||
|
PathPiece
|
||||||
|
Pretty
|
||||||
|
Settings
|
||||||
|
Settings.StaticFiles
|
||||||
|
Types
|
||||||
|
other-modules:
|
||||||
|
Paths_espial
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
executable espial
|
||||||
|
main-is: main.hs
|
||||||
|
hs-source-dirs:
|
||||||
|
app
|
||||||
|
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
|
||||||
|
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||||
|
build-depends:
|
||||||
|
aeson >=1.4
|
||||||
|
, attoparsec
|
||||||
|
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
|
||||||
|
, bcrypt >=0.0.8
|
||||||
|
, bytestring >=0.9 && <0.11
|
||||||
|
, case-insensitive
|
||||||
|
, classy-prelude >=1.4 && <1.6
|
||||||
|
, classy-prelude-conduit >=1.4 && <1.6
|
||||||
|
, classy-prelude-yesod >=1.4 && <1.6
|
||||||
|
, conduit >=1.0 && <2.0
|
||||||
|
, containers
|
||||||
|
, data-default
|
||||||
|
, directory >=1.1 && <1.4
|
||||||
|
, ekg
|
||||||
|
, ekg-core
|
||||||
|
, entropy
|
||||||
|
, espial
|
||||||
|
, esqueleto
|
||||||
|
, fast-logger >=2.2 && <2.5
|
||||||
|
, file-embed
|
||||||
|
, foreign-store
|
||||||
|
, hjsmin >=0.1 && <0.3
|
||||||
|
, hscolour
|
||||||
|
, http-api-data >=0.3.4
|
||||||
|
, http-client
|
||||||
|
, http-client-tls >=0.3 && <0.4
|
||||||
|
, http-conduit >=2.3 && <2.4
|
||||||
|
, http-types
|
||||||
|
, iso8601-time >=0.1.3
|
||||||
|
, microlens
|
||||||
|
, monad-logger >=0.3 && <0.4
|
||||||
|
, monad-metrics
|
||||||
|
, mtl
|
||||||
|
, parser-combinators
|
||||||
|
, persistent >=2.8 && <2.10
|
||||||
|
, persistent-sqlite >=2.6.2
|
||||||
|
, persistent-template >=2.5 && <2.9
|
||||||
|
, pretty-show
|
||||||
|
, safe
|
||||||
|
, shakespeare >=2.0 && <2.1
|
||||||
|
, template-haskell
|
||||||
|
, text >=0.11 && <2.0
|
||||||
|
, time
|
||||||
|
, transformers >=0.2.2
|
||||||
|
, unordered-containers
|
||||||
|
, vector
|
||||||
|
, wai
|
||||||
|
, wai-extra >=3.0 && <3.1
|
||||||
|
, wai-logger >=2.2 && <2.4
|
||||||
|
, wai-middleware-metrics
|
||||||
|
, warp >=3.0 && <3.3
|
||||||
|
, yaml >=0.8 && <0.12
|
||||||
|
, yesod >=1.6 && <1.7
|
||||||
|
, yesod-auth >=1.6 && <1.7
|
||||||
|
, yesod-core >=1.6 && <1.7
|
||||||
|
, yesod-form >=1.6 && <1.7
|
||||||
|
, yesod-static >=1.6 && <1.7
|
||||||
|
if flag(library-only)
|
||||||
|
buildable: False
|
||||||
|
other-modules:
|
||||||
|
DevelMain
|
||||||
|
Paths_espial
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
executable migration
|
||||||
|
main-is: Main.hs
|
||||||
|
hs-source-dirs:
|
||||||
|
app/migration
|
||||||
|
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
|
||||||
|
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||||
|
build-depends:
|
||||||
|
aeson >=1.4
|
||||||
|
, attoparsec
|
||||||
|
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
|
||||||
|
, bcrypt >=0.0.8
|
||||||
|
, bytestring >=0.9 && <0.11
|
||||||
|
, case-insensitive
|
||||||
|
, classy-prelude >=1.4 && <1.6
|
||||||
|
, classy-prelude-conduit >=1.4 && <1.6
|
||||||
|
, classy-prelude-yesod >=1.4 && <1.6
|
||||||
|
, conduit >=1.0 && <2.0
|
||||||
|
, containers
|
||||||
|
, data-default
|
||||||
|
, directory >=1.1 && <1.4
|
||||||
|
, ekg
|
||||||
|
, ekg-core
|
||||||
|
, entropy
|
||||||
|
, espial
|
||||||
|
, esqueleto
|
||||||
|
, fast-logger >=2.2 && <2.5
|
||||||
|
, file-embed
|
||||||
|
, foreign-store
|
||||||
|
, hjsmin >=0.1 && <0.3
|
||||||
|
, hscolour
|
||||||
|
, http-api-data >=0.3.4
|
||||||
|
, http-client
|
||||||
|
, http-client-tls >=0.3 && <0.4
|
||||||
|
, http-conduit >=2.3 && <2.4
|
||||||
|
, http-types
|
||||||
|
, iso8601-time >=0.1.3
|
||||||
|
, microlens
|
||||||
|
, monad-logger >=0.3 && <0.4
|
||||||
|
, monad-metrics
|
||||||
|
, mtl
|
||||||
|
, optparse-generic >=1.2.3
|
||||||
|
, parser-combinators
|
||||||
|
, persistent >=2.8 && <2.10
|
||||||
|
, persistent-sqlite >=2.6.2
|
||||||
|
, persistent-template >=2.5 && <2.9
|
||||||
|
, pretty-show
|
||||||
|
, safe
|
||||||
|
, shakespeare >=2.0 && <2.1
|
||||||
|
, template-haskell
|
||||||
|
, text >=0.11 && <2.0
|
||||||
|
, time
|
||||||
|
, transformers >=0.2.2
|
||||||
|
, unordered-containers
|
||||||
|
, vector
|
||||||
|
, wai
|
||||||
|
, wai-extra >=3.0 && <3.1
|
||||||
|
, wai-logger >=2.2 && <2.4
|
||||||
|
, wai-middleware-metrics
|
||||||
|
, warp >=3.0 && <3.3
|
||||||
|
, yaml >=0.8 && <0.12
|
||||||
|
, yesod >=1.6 && <1.7
|
||||||
|
, yesod-auth >=1.6 && <1.7
|
||||||
|
, yesod-core >=1.6 && <1.7
|
||||||
|
, yesod-form >=1.6 && <1.7
|
||||||
|
, yesod-static >=1.6 && <1.7
|
||||||
|
if flag(library-only)
|
||||||
|
buildable: False
|
||||||
|
other-modules:
|
||||||
|
Paths_espial
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
test-suite test
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
main-is: Spec.hs
|
||||||
|
hs-source-dirs:
|
||||||
|
test
|
||||||
|
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
|
||||||
|
ghc-options: -Wall
|
||||||
|
build-depends:
|
||||||
|
aeson >=1.4
|
||||||
|
, attoparsec
|
||||||
|
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
|
||||||
|
, bcrypt >=0.0.8
|
||||||
|
, bytestring >=0.9 && <0.11
|
||||||
|
, case-insensitive
|
||||||
|
, classy-prelude >=1.4 && <1.6
|
||||||
|
, classy-prelude-conduit >=1.4 && <1.6
|
||||||
|
, classy-prelude-yesod >=1.4 && <1.6
|
||||||
|
, conduit >=1.0 && <2.0
|
||||||
|
, containers
|
||||||
|
, data-default
|
||||||
|
, directory >=1.1 && <1.4
|
||||||
|
, ekg
|
||||||
|
, ekg-core
|
||||||
|
, entropy
|
||||||
|
, espial
|
||||||
|
, esqueleto
|
||||||
|
, fast-logger >=2.2 && <2.5
|
||||||
|
, file-embed
|
||||||
|
, foreign-store
|
||||||
|
, hjsmin >=0.1 && <0.3
|
||||||
|
, hscolour
|
||||||
|
, hspec >=2.0.0
|
||||||
|
, http-api-data >=0.3.4
|
||||||
|
, http-client
|
||||||
|
, http-client-tls >=0.3 && <0.4
|
||||||
|
, http-conduit >=2.3 && <2.4
|
||||||
|
, http-types
|
||||||
|
, iso8601-time >=0.1.3
|
||||||
|
, microlens
|
||||||
|
, monad-logger >=0.3 && <0.4
|
||||||
|
, monad-metrics
|
||||||
|
, mtl
|
||||||
|
, parser-combinators
|
||||||
|
, persistent >=2.8 && <2.10
|
||||||
|
, persistent-sqlite >=2.6.2
|
||||||
|
, persistent-template >=2.5 && <2.9
|
||||||
|
, pretty-show
|
||||||
|
, safe
|
||||||
|
, shakespeare >=2.0 && <2.1
|
||||||
|
, template-haskell
|
||||||
|
, text >=0.11 && <2.0
|
||||||
|
, time
|
||||||
|
, transformers >=0.2.2
|
||||||
|
, unordered-containers
|
||||||
|
, vector
|
||||||
|
, wai
|
||||||
|
, wai-extra >=3.0 && <3.1
|
||||||
|
, wai-logger >=2.2 && <2.4
|
||||||
|
, wai-middleware-metrics
|
||||||
|
, warp >=3.0 && <3.3
|
||||||
|
, yaml >=0.8 && <0.12
|
||||||
|
, yesod >=1.6 && <1.7
|
||||||
|
, yesod-auth >=1.6 && <1.7
|
||||||
|
, yesod-core >=1.6 && <1.7
|
||||||
|
, yesod-form >=1.6 && <1.7
|
||||||
|
, yesod-static >=1.6 && <1.7
|
||||||
|
, yesod-test
|
||||||
|
other-modules:
|
||||||
|
Handler.CommonSpec
|
||||||
|
Handler.HomeSpec
|
||||||
|
TestImport
|
||||||
|
Paths_espial
|
||||||
|
default-language: Haskell2010
|
208
package.yaml
Normal file
208
package.yaml
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
name: espial
|
||||||
|
synopsis: Espial is an open-source, web-based bookmarking server.
|
||||||
|
version: "0.0.7"
|
||||||
|
description: ! '
|
||||||
|
|
||||||
|
Espial is an open-source, web-based bookmarking server.
|
||||||
|
|
||||||
|
- Yesod + PureScript + sqlite3
|
||||||
|
|
||||||
|
- multi-user (w/ privacy scopes)
|
||||||
|
|
||||||
|
- tags, stars, editing, deleting
|
||||||
|
'
|
||||||
|
category: Web
|
||||||
|
author: Jon Schoning
|
||||||
|
maintainer: jonschoning@gmail.com
|
||||||
|
copyright: Copyright (c) 2018 Jon Schoning
|
||||||
|
license: AGPL-3
|
||||||
|
license-file: LICENSE
|
||||||
|
homepage: https://github.com/jonschoning/espial
|
||||||
|
git: git://github.com/jonschoning/espial.git
|
||||||
|
bug-reports: https://github.com/jonschoning/espial/issues
|
||||||
|
extra-source-files:
|
||||||
|
- README.md
|
||||||
|
- changelog.md
|
||||||
|
- config/favicon.ico
|
||||||
|
- config/keter.yml
|
||||||
|
- config/robots.txt
|
||||||
|
- config/routes
|
||||||
|
- config/settings.yml
|
||||||
|
- config/test-settings.yml
|
||||||
|
- templates/**
|
||||||
|
- static/css/**
|
||||||
|
- static/images/**
|
||||||
|
- static/js/**
|
||||||
|
- purs/Makefile
|
||||||
|
- purs/packages.dhall
|
||||||
|
- purs/spago.dhall
|
||||||
|
- purs/src/**
|
||||||
|
- purs/src/**/Component/**
|
||||||
|
- purs/test/**
|
||||||
|
|
||||||
|
default-extensions:
|
||||||
|
- BangPatterns
|
||||||
|
- CPP
|
||||||
|
- ConstraintKinds
|
||||||
|
- DataKinds
|
||||||
|
- DeriveDataTypeable
|
||||||
|
- DeriveGeneric
|
||||||
|
- EmptyDataDecls
|
||||||
|
- FlexibleContexts
|
||||||
|
- FlexibleInstances
|
||||||
|
- GADTs
|
||||||
|
- GeneralizedNewtypeDeriving
|
||||||
|
- InstanceSigs
|
||||||
|
- KindSignatures
|
||||||
|
- LambdaCase
|
||||||
|
- MultiParamTypeClasses
|
||||||
|
- MultiWayIf
|
||||||
|
- NoImplicitPrelude
|
||||||
|
- OverloadedStrings
|
||||||
|
- PolyKinds
|
||||||
|
- PolymorphicComponents
|
||||||
|
- QuasiQuotes
|
||||||
|
- Rank2Types
|
||||||
|
- RankNTypes
|
||||||
|
- RecordWildCards
|
||||||
|
- ScopedTypeVariables
|
||||||
|
- StandaloneDeriving
|
||||||
|
- TemplateHaskell
|
||||||
|
- TupleSections
|
||||||
|
- TypeApplications
|
||||||
|
- TypeFamilies
|
||||||
|
- TypeOperators
|
||||||
|
- TypeSynonymInstances
|
||||||
|
- ViewPatterns
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
|
||||||
|
# Due to a bug in GHC 8.0.1, we block its usage
|
||||||
|
# See: https://ghc.haskell.org/trac/ghc/ticket/12130
|
||||||
|
- base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
|
||||||
|
|
||||||
|
- yesod >=1.6 && <1.7
|
||||||
|
- yesod-core >=1.6 && <1.7
|
||||||
|
- yesod-auth >=1.6 && <1.7
|
||||||
|
- yesod-static >=1.6 && <1.7
|
||||||
|
- yesod-form >=1.6 && <1.7
|
||||||
|
- classy-prelude >=1.4 && <1.6
|
||||||
|
- classy-prelude-conduit >=1.4 && <1.6
|
||||||
|
- classy-prelude-yesod >=1.4 && <1.6
|
||||||
|
- bytestring >=0.9 && <0.11
|
||||||
|
- text >=0.11 && <2.0
|
||||||
|
- persistent >=2.8 && <2.10
|
||||||
|
# - persistent-postgresql >=2.8 && <2.9
|
||||||
|
- persistent-template >=2.5 && <2.9
|
||||||
|
- template-haskell
|
||||||
|
- shakespeare >=2.0 && <2.1
|
||||||
|
- hjsmin >=0.1 && <0.3
|
||||||
|
# - monad-control >=0.3 && <1.1
|
||||||
|
- wai-extra >=3.0 && <3.1
|
||||||
|
- yaml >=0.8 && <0.12
|
||||||
|
- http-client-tls >=0.3 && <0.4
|
||||||
|
- http-conduit >=2.3 && <2.4
|
||||||
|
- directory >=1.1 && <1.4
|
||||||
|
- warp >=3.0 && <3.3
|
||||||
|
- data-default
|
||||||
|
# - aeson >=0.6 && <1.4
|
||||||
|
- conduit >=1.0 && <2.0
|
||||||
|
- monad-logger >=0.3 && <0.4
|
||||||
|
- fast-logger >=2.2 && <2.5
|
||||||
|
- wai-logger >=2.2 && <2.4
|
||||||
|
- file-embed
|
||||||
|
- safe
|
||||||
|
- unordered-containers
|
||||||
|
- containers
|
||||||
|
- vector
|
||||||
|
- time
|
||||||
|
- case-insensitive
|
||||||
|
- wai
|
||||||
|
- foreign-store
|
||||||
|
|
||||||
|
- aeson >=1.4
|
||||||
|
- attoparsec
|
||||||
|
- bcrypt >= 0.0.8
|
||||||
|
- entropy
|
||||||
|
- ekg
|
||||||
|
- ekg-core
|
||||||
|
- esqueleto
|
||||||
|
- hscolour
|
||||||
|
- http-api-data >= 0.3.4
|
||||||
|
- http-client
|
||||||
|
- http-types
|
||||||
|
- iso8601-time >=0.1.3
|
||||||
|
- microlens
|
||||||
|
- monad-metrics
|
||||||
|
- mtl
|
||||||
|
- persistent-sqlite >=2.6.2
|
||||||
|
- pretty-show
|
||||||
|
- transformers >= 0.2.2
|
||||||
|
- wai-middleware-metrics
|
||||||
|
- parser-combinators
|
||||||
|
|
||||||
|
# The library contains all of our application code. The executable
|
||||||
|
# defined below is just a thin wrapper.
|
||||||
|
library:
|
||||||
|
source-dirs: src
|
||||||
|
when:
|
||||||
|
- condition: (flag(dev)) || (flag(library-only))
|
||||||
|
then:
|
||||||
|
ghc-options:
|
||||||
|
- -Wall
|
||||||
|
- -fwarn-tabs
|
||||||
|
- -O0
|
||||||
|
cpp-options: -DDEVELOPMENT
|
||||||
|
else:
|
||||||
|
ghc-options:
|
||||||
|
- -Wall
|
||||||
|
- -fwarn-tabs
|
||||||
|
- -O2
|
||||||
|
|
||||||
|
# Runnable executable for our application
|
||||||
|
executables:
|
||||||
|
espial:
|
||||||
|
main: main.hs
|
||||||
|
source-dirs: app
|
||||||
|
ghc-options:
|
||||||
|
- -threaded
|
||||||
|
- -rtsopts
|
||||||
|
- -with-rtsopts=-N
|
||||||
|
dependencies:
|
||||||
|
- espial
|
||||||
|
when:
|
||||||
|
- condition: flag(library-only)
|
||||||
|
buildable: false
|
||||||
|
migration:
|
||||||
|
when:
|
||||||
|
- condition: flag(library-only)
|
||||||
|
buildable: false
|
||||||
|
main: Main.hs
|
||||||
|
source-dirs:
|
||||||
|
- app/migration
|
||||||
|
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||||
|
dependencies:
|
||||||
|
- espial
|
||||||
|
- optparse-generic >= 1.2.3
|
||||||
|
|
||||||
|
# Test suite
|
||||||
|
tests:
|
||||||
|
test:
|
||||||
|
main: Spec.hs
|
||||||
|
source-dirs: test
|
||||||
|
ghc-options: -Wall
|
||||||
|
dependencies:
|
||||||
|
- espial
|
||||||
|
- hspec >=2.0.0
|
||||||
|
- yesod-test
|
||||||
|
|
||||||
|
# Define flags used by "yesod devel" to make compilation faster
|
||||||
|
flags:
|
||||||
|
library-only:
|
||||||
|
description: Build for use with "yesod devel"
|
||||||
|
manual: false
|
||||||
|
default: false
|
||||||
|
dev:
|
||||||
|
description: Turn on development settings, like auto-reload templates.
|
||||||
|
manual: false
|
||||||
|
default: false
|
11
purs/.gitignore
vendored
Normal file
11
purs/.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/.psa*
|
||||||
|
/.psc*
|
||||||
|
/.psc-package/
|
||||||
|
/.pulp-cache/
|
||||||
|
/.purs*
|
||||||
|
/.spago
|
||||||
|
/bower_components/
|
||||||
|
/generated-docs/
|
||||||
|
/node_modules/
|
||||||
|
/output/
|
||||||
|
/tmp/
|
28
purs/Makefile
Normal file
28
purs/Makefile
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
.PHONY: clean build
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
install:
|
||||||
|
spago install
|
||||||
|
|
||||||
|
build:
|
||||||
|
@spago build
|
||||||
|
@spago bundle --to dist/app.js
|
||||||
|
@(cd dist && terser app.js -m -c -o app.min.js)
|
||||||
|
@rm -f dist/*.gz
|
||||||
|
@gzip -k dist/app.js
|
||||||
|
@gzip -k dist/app.min.js
|
||||||
|
@find dist -type f -printf "%kK\\t%h/%f\\n" | sort -k 2
|
||||||
|
@cp dist/app.js ../static/js/app.js
|
||||||
|
@cp dist/app.js.gz ../static/js/app.js.gz
|
||||||
|
@cp dist/app.min.js ../static/js/app.min.js
|
||||||
|
@cp dist/app.min.js.gz ../static/js/app.min.js.gz
|
||||||
|
|
||||||
|
docs:
|
||||||
|
@rm -Rf generated-docs
|
||||||
|
@purs docs ".spago/*/*/src/**/*.purs" --format html
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f dist/*
|
||||||
|
|
||||||
|
# inotifywait -m -r -q -e close_write --format '%T %w%f' --timefmt '%T' src | while read FILE; do echo $FILE; make; done
|
29
purs/README.md
Normal file
29
purs/README.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
## Development (Posix only)
|
||||||
|
|
||||||
|
1. Install `purescript`, `purescript-spago`, `terser`:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. (optional) working with .dhall files:
|
||||||
|
|
||||||
|
```
|
||||||
|
stack install dhall dhall-json
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Download purescript libraries (1x only):
|
||||||
|
|
||||||
|
```
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
4. build dist/app.min.js:
|
||||||
|
|
||||||
|
```
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
On a successful build, `make` will also update `../static/js/`,
|
||||||
|
since the `purs/` folder is opaque to the espial executable build process.
|
||||||
|
|
1280
purs/package-lock.json
generated
Normal file
1280
purs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
15
purs/package.json
Normal file
15
purs/package.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "espial",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"make-install": "make install",
|
||||||
|
"make-watch": "inotifywait -m -r -q -e close_write --format '%T %w%f' --timefmt '%T' src | while read FILE; do echo $FILE; make; done",
|
||||||
|
"make": "make"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"purescript": "^0.12.1",
|
||||||
|
"purescript-spago": "^0.6.2",
|
||||||
|
"terser": "^3.14.1"
|
||||||
|
},
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
121
purs/packages.dhall
Normal file
121
purs/packages.dhall
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
{-
|
||||||
|
Welcome to Spacchetti local packages!
|
||||||
|
|
||||||
|
Below are instructions for how to edit this file for most use
|
||||||
|
cases, so that you don't need to know Dhall to use it.
|
||||||
|
|
||||||
|
## Warning: Don't Move This Top-Level Comment!
|
||||||
|
|
||||||
|
Due to how `dhall format` currently works, this comment's
|
||||||
|
instructions cannot appear near corresponding sections below
|
||||||
|
because `dhall format` will delete the comment. However,
|
||||||
|
it will not delete a top-level comment like this one.
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
Most will want to do one or both of these options:
|
||||||
|
1. Override/Patch a package's dependency
|
||||||
|
2. Add a package not already in the default package set
|
||||||
|
|
||||||
|
This file will continue to work whether you use one or both options.
|
||||||
|
Instructions for each option are explained below.
|
||||||
|
|
||||||
|
### Overriding/Patching a package
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
- Change a package's dependency to a newer/older release than the
|
||||||
|
default package set's release
|
||||||
|
- Use your own modified version of some dependency that may
|
||||||
|
include new API, changed API, removed API by
|
||||||
|
using your custom git repo of the library rather than
|
||||||
|
the package set's repo
|
||||||
|
|
||||||
|
Syntax:
|
||||||
|
Replace the overrides' "{=}" (an empty record) with the following idea
|
||||||
|
The "//" or "⫽" means "merge these two records and
|
||||||
|
when they have the same value, use the one on the right:"
|
||||||
|
-------------------------------
|
||||||
|
let override =
|
||||||
|
{ packageName =
|
||||||
|
upstream.packageName ⫽ { updateEntity1 = "new value", updateEntity2 = "new value" }
|
||||||
|
, packageName =
|
||||||
|
upstream.packageName ⫽ { version = "v4.0.0" }
|
||||||
|
, packageName =
|
||||||
|
upstream.packageName // { repo = "https://www.example.com/path/to/new/repo.git" }
|
||||||
|
}
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Example:
|
||||||
|
-------------------------------
|
||||||
|
let overrides =
|
||||||
|
{ halogen =
|
||||||
|
upstream.halogen ⫽ { version = "master" }
|
||||||
|
, halogen-vdom =
|
||||||
|
upstream.halogen-vdom ⫽ { version = "v4.0.0" }
|
||||||
|
}
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
### Additions
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
- Add packages that aren't alread included in the default package set
|
||||||
|
|
||||||
|
Syntax:
|
||||||
|
Replace the additions' "{=}" (an empty record) with the following idea:
|
||||||
|
-------------------------------
|
||||||
|
let additions =
|
||||||
|
{ "package-name" =
|
||||||
|
mkPackage
|
||||||
|
[ "dependency1"
|
||||||
|
, "dependency2"
|
||||||
|
]
|
||||||
|
"https://example.com/path/to/git/repo.git"
|
||||||
|
"tag ('v4.0.0') or branch ('master')"
|
||||||
|
, "package-name" =
|
||||||
|
mkPackage
|
||||||
|
[ "dependency1"
|
||||||
|
, "dependency2"
|
||||||
|
]
|
||||||
|
"https://example.com/path/to/git/repo.git"
|
||||||
|
"tag ('v4.0.0') or branch ('master')"
|
||||||
|
, etc.
|
||||||
|
}
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Example:
|
||||||
|
-------------------------------
|
||||||
|
let additions =
|
||||||
|
{ benchotron =
|
||||||
|
mkPackage
|
||||||
|
[ "arrays"
|
||||||
|
, "exists"
|
||||||
|
, "profunctor"
|
||||||
|
, "strings"
|
||||||
|
, "quickcheck"
|
||||||
|
, "lcg"
|
||||||
|
, "transformers"
|
||||||
|
, "foldable-traversable"
|
||||||
|
, "exceptions"
|
||||||
|
, "node-fs"
|
||||||
|
, "node-buffer"
|
||||||
|
, "node-readline"
|
||||||
|
, "datetime"
|
||||||
|
, "now"
|
||||||
|
]
|
||||||
|
"https://github.com/hdgarrood/purescript-benchotron.git"
|
||||||
|
"v7.0.0"
|
||||||
|
}
|
||||||
|
-------------------------------
|
||||||
|
-}
|
||||||
|
|
||||||
|
let mkPackage =
|
||||||
|
https://raw.githubusercontent.com/spacchetti/spacchetti/20190105/src/mkPackage.dhall sha256:8e1c6636f8a089f972b21cde0cef4b33fa36a2e503ad4c77928aabf92d2d4ec9
|
||||||
|
|
||||||
|
let upstream =
|
||||||
|
https://raw.githubusercontent.com/spacchetti/spacchetti/20190105/src/packages.dhall sha256:38fc3e19c193bb006c773ac84fc4a2888e5dcc610d36e49a9bdef7ecc7e1f8c9
|
||||||
|
|
||||||
|
let overrides = {=}
|
||||||
|
|
||||||
|
let additions = {=}
|
||||||
|
|
||||||
|
in upstream ⫽ overrides ⫽ additions
|
24
purs/spago.dhall
Normal file
24
purs/spago.dhall
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{ name =
|
||||||
|
"espial"
|
||||||
|
, dependencies =
|
||||||
|
[ "aff"
|
||||||
|
, "simple-json"
|
||||||
|
, "affjax"
|
||||||
|
, "argonaut"
|
||||||
|
, "arrays"
|
||||||
|
, "console"
|
||||||
|
, "debug"
|
||||||
|
, "effect"
|
||||||
|
, "either"
|
||||||
|
, "functions"
|
||||||
|
, "halogen"
|
||||||
|
, "prelude"
|
||||||
|
, "psci-support"
|
||||||
|
, "strings"
|
||||||
|
, "transformers"
|
||||||
|
, "web-html"
|
||||||
|
, "profunctor-lenses"
|
||||||
|
]
|
||||||
|
, packages =
|
||||||
|
./packages.dhall
|
||||||
|
}
|
119
purs/src/App.purs
Normal file
119
purs/src/App.purs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
module App where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Affjax (Response, ResponseFormatError)
|
||||||
|
import Affjax (defaultRequest) as AX
|
||||||
|
import Affjax as Ax
|
||||||
|
import Affjax.RequestBody as AXReq
|
||||||
|
import Affjax.RequestHeader (RequestHeader(..))
|
||||||
|
import Affjax.ResponseFormat as AXRes
|
||||||
|
import Data.Argonaut (Json)
|
||||||
|
import Data.Array ((:))
|
||||||
|
import Data.Either (Either(..))
|
||||||
|
import Data.FormURLEncoded (FormURLEncoded)
|
||||||
|
import Data.HTTP.Method (Method(..))
|
||||||
|
import Data.Maybe (Maybe(..))
|
||||||
|
import Data.MediaType.Common (applicationFormURLEncoded, applicationJSON)
|
||||||
|
import Effect.Aff (Aff)
|
||||||
|
import Effect.Class (liftEffect)
|
||||||
|
import Globals (app')
|
||||||
|
import Model (Bookmark, Bookmark'(..), Note, Note'(..), AccountSettings, AccountSettings'(..))
|
||||||
|
import Simple.JSON as J
|
||||||
|
import Web.HTML (window)
|
||||||
|
import Web.HTML.Location (reload)
|
||||||
|
import Web.HTML.Window (location)
|
||||||
|
|
||||||
|
data StarAction = Star | UnStar
|
||||||
|
instance showStar :: Show StarAction where
|
||||||
|
show Star = "star"
|
||||||
|
show UnStar = "unstar"
|
||||||
|
|
||||||
|
toggleStar :: Int -> StarAction -> Aff Unit
|
||||||
|
toggleStar bid action = do
|
||||||
|
let path = "bm/" <> show bid <> "/" <> show action
|
||||||
|
void (fetchUrlEnc POST path Nothing AXRes.ignore)
|
||||||
|
|
||||||
|
destroy :: Int -> Aff (Response (Either ResponseFormatError Unit))
|
||||||
|
destroy bid =
|
||||||
|
fetchUrlEnc DELETE ("bm/" <> show bid) Nothing AXRes.ignore
|
||||||
|
|
||||||
|
markRead :: Int -> Aff (Response (Either ResponseFormatError Unit))
|
||||||
|
markRead bid = do
|
||||||
|
let path = "bm/" <> show bid <> "/read"
|
||||||
|
fetchUrlEnc POST path Nothing AXRes.ignore
|
||||||
|
|
||||||
|
editBookmark :: Bookmark -> Aff (Response (Either ResponseFormatError Unit))
|
||||||
|
editBookmark bm = do
|
||||||
|
fetchJson POST "api/add" (Just (Bookmark' bm)) AXRes.ignore
|
||||||
|
|
||||||
|
editNote :: Note -> Aff (Response (Either ResponseFormatError Json))
|
||||||
|
editNote bm = do
|
||||||
|
fetchJson POST "api/note/add" (Just (Note' bm)) AXRes.json
|
||||||
|
|
||||||
|
destroyNote :: Int -> Aff (Response (Either ResponseFormatError Unit))
|
||||||
|
destroyNote nid = do
|
||||||
|
fetchUrlEnc DELETE ("api/note/" <> show nid) Nothing AXRes.ignore
|
||||||
|
|
||||||
|
editAccountSettings :: AccountSettings -> Aff (Response (Either ResponseFormatError Unit))
|
||||||
|
editAccountSettings us = do
|
||||||
|
fetchJson POST "api/accountSettings" (Just (AccountSettings' us)) AXRes.ignore
|
||||||
|
|
||||||
|
logout :: Unit -> Aff Unit
|
||||||
|
logout u = do
|
||||||
|
void (fetchUrl POST app.authRlogoutR [] Nothing AXRes.ignore)
|
||||||
|
liftEffect (window >>= location >>= reload)
|
||||||
|
where
|
||||||
|
app = app' u
|
||||||
|
|
||||||
|
fetchJson
|
||||||
|
:: forall a b.
|
||||||
|
J.WriteForeign b
|
||||||
|
=> Method
|
||||||
|
-> String
|
||||||
|
-> Maybe b
|
||||||
|
-> AXRes.ResponseFormat a
|
||||||
|
-> Aff (Response (Either ResponseFormatError a))
|
||||||
|
fetchJson method path content rt =
|
||||||
|
fetchPath method path [ContentType applicationJSON] (AXReq.string <<< J.writeJSON <$> content) rt
|
||||||
|
|
||||||
|
fetchUrlEnc
|
||||||
|
:: forall a.
|
||||||
|
Method
|
||||||
|
-> String
|
||||||
|
-> Maybe FormURLEncoded
|
||||||
|
-> AXRes.ResponseFormat a
|
||||||
|
-> Aff (Response (Either ResponseFormatError a))
|
||||||
|
fetchUrlEnc method path content rt =
|
||||||
|
fetchPath method path [ContentType applicationFormURLEncoded] (AXReq.FormURLEncoded <$> content) rt
|
||||||
|
|
||||||
|
fetchPath
|
||||||
|
:: forall a.
|
||||||
|
Method
|
||||||
|
-> String
|
||||||
|
-> Array RequestHeader
|
||||||
|
-> Maybe AXReq.RequestBody
|
||||||
|
-> AXRes.ResponseFormat a
|
||||||
|
-> Aff (Response (Either ResponseFormatError a))
|
||||||
|
fetchPath method path headers content rt =
|
||||||
|
fetchUrl method ((app' unit).homeR <> path) headers content rt
|
||||||
|
|
||||||
|
fetchUrl
|
||||||
|
:: forall a.
|
||||||
|
Method
|
||||||
|
-> String
|
||||||
|
-> Array RequestHeader
|
||||||
|
-> Maybe AXReq.RequestBody
|
||||||
|
-> AXRes.ResponseFormat a
|
||||||
|
-> Aff (Response (Either ResponseFormatError a))
|
||||||
|
fetchUrl method url headers content rt =
|
||||||
|
Ax.request
|
||||||
|
AX.defaultRequest
|
||||||
|
{ url = url
|
||||||
|
, method = Left method
|
||||||
|
, headers = RequestHeader app.csrfHeaderName app.csrfToken : headers
|
||||||
|
, content = content
|
||||||
|
, responseFormat = rt
|
||||||
|
}
|
||||||
|
where
|
||||||
|
app = app' unit
|
91
purs/src/Component/AccountSettings.purs
Normal file
91
purs/src/Component/AccountSettings.purs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
module Component.AccountSettings where
|
||||||
|
|
||||||
|
import Prelude hiding (div)
|
||||||
|
|
||||||
|
import App (editAccountSettings)
|
||||||
|
import Data.Lens (Lens', lens, use, (%=))
|
||||||
|
import Data.Maybe (Maybe(..))
|
||||||
|
import Effect.Aff (Aff)
|
||||||
|
import Globals (app')
|
||||||
|
import Halogen as H
|
||||||
|
import Halogen.HTML (HTML, div, input, text)
|
||||||
|
import Halogen.HTML.Elements (label)
|
||||||
|
import Halogen.HTML.Events (onChecked)
|
||||||
|
import Halogen.HTML.Events as HE
|
||||||
|
import Halogen.HTML.Properties (InputType(..), checked, for, id_, name, type_)
|
||||||
|
import Model (AccountSettings)
|
||||||
|
import Util (class_)
|
||||||
|
import Web.Event.Event (Event)
|
||||||
|
|
||||||
|
type UState =
|
||||||
|
{ us :: AccountSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
_us :: Lens' UState AccountSettings
|
||||||
|
_us = lens _.us (_ { us = _ })
|
||||||
|
|
||||||
|
data UQuery a
|
||||||
|
= UEditField EditField a
|
||||||
|
| USubmit Event a
|
||||||
|
|
||||||
|
data EditField
|
||||||
|
= EarchiveDefault Boolean
|
||||||
|
| EprivateDefault Boolean
|
||||||
|
| EprivacyLock Boolean
|
||||||
|
|
||||||
|
|
||||||
|
-- | The bookmark component definition.
|
||||||
|
usetting :: AccountSettings -> H.Component HTML UQuery Unit Unit Aff
|
||||||
|
usetting u' =
|
||||||
|
H.component
|
||||||
|
{ initialState: const (mkState u')
|
||||||
|
, render
|
||||||
|
, eval
|
||||||
|
, receiver: const Nothing
|
||||||
|
}
|
||||||
|
where
|
||||||
|
app = app' unit
|
||||||
|
|
||||||
|
mkState u =
|
||||||
|
{ us: u
|
||||||
|
}
|
||||||
|
|
||||||
|
render :: UState -> H.ComponentHTML UQuery
|
||||||
|
render { us } =
|
||||||
|
div [ class_ "settings-form" ]
|
||||||
|
[ div [ class_ "fw7 mb2"] [ text "Account Settings" ]
|
||||||
|
, div [ class_ "flex items-center mb2" ]
|
||||||
|
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "archiveDefault", name "archiveDefault"
|
||||||
|
, checked (us.archiveDefault) , onChecked (editField EarchiveDefault) ]
|
||||||
|
, label [ for "archiveDefault", class_ "lh-copy" ]
|
||||||
|
[ text "Archive Non-Private Bookmarks (archive.li)" ]
|
||||||
|
]
|
||||||
|
, div [ class_ "flex items-center mb2" ]
|
||||||
|
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "privateDefault", name "privateDefault"
|
||||||
|
, checked (us.privateDefault) , onChecked (editField EprivateDefault) ]
|
||||||
|
, label [ for "privateDefault", class_ "lh-copy" ]
|
||||||
|
[ text "Default new bookmarks to Private" ]
|
||||||
|
]
|
||||||
|
, div [ class_ "flex items-center mb2" ]
|
||||||
|
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "privacyLock", name "privacyLock"
|
||||||
|
, checked (us.privacyLock) , onChecked (editField EprivacyLock) ]
|
||||||
|
, label [ for "privacyLock", class_ "lh-copy" ]
|
||||||
|
[ text "Privacy Lock (Private Account)" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
where
|
||||||
|
editField :: forall a. (a -> EditField) -> a -> Maybe (UQuery Unit)
|
||||||
|
editField f = HE.input UEditField <<< f
|
||||||
|
|
||||||
|
eval :: UQuery ~> H.ComponentDSL UState UQuery Unit Aff
|
||||||
|
eval (UEditField f next) = do
|
||||||
|
_us %= case f of
|
||||||
|
EarchiveDefault e -> _ { archiveDefault = e }
|
||||||
|
EprivateDefault e -> _ { privateDefault = e }
|
||||||
|
EprivacyLock e -> _ { privacyLock = e }
|
||||||
|
pure next
|
||||||
|
|
||||||
|
eval (USubmit e next) = do
|
||||||
|
us <- use _us
|
||||||
|
void $ H.liftAff (editAccountSettings us)
|
||||||
|
pure next
|
179
purs/src/Component/Add.purs
Normal file
179
purs/src/Component/Add.purs
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
module Component.Add where
|
||||||
|
|
||||||
|
import Prelude hiding (div)
|
||||||
|
|
||||||
|
import App (destroy, editBookmark)
|
||||||
|
import Data.Array (drop, foldMap)
|
||||||
|
import Data.Lens (Lens', lens, use, (%=), (.=))
|
||||||
|
import Data.Maybe (Maybe(..), maybe)
|
||||||
|
import Data.Monoid (guard)
|
||||||
|
import Data.String (null)
|
||||||
|
import Data.String (split) as S
|
||||||
|
import Data.String.Pattern (Pattern(..))
|
||||||
|
import Data.Tuple (fst, snd)
|
||||||
|
import Effect.Aff (Aff)
|
||||||
|
import Effect.Class (liftEffect)
|
||||||
|
import Globals (app', closeWindow, mmoment8601)
|
||||||
|
import Halogen as H
|
||||||
|
import Halogen.HTML (HTML, br_, button, div, div_, form, input, label, p, span, table, tbody_, td, td_, text, textarea, tr_)
|
||||||
|
import Halogen.HTML.Events (onSubmit, onValueChange, onChecked, onClick)
|
||||||
|
import Halogen.HTML.Events as HE
|
||||||
|
import Halogen.HTML.Properties (autofocus, ButtonType(..), InputType(..), autocomplete, checked, for, id_, name, required, rows, title, type_, value)
|
||||||
|
import Model (Bookmark)
|
||||||
|
import Util (_curQuerystring, _loc, _lookupQueryStringValue, attr, class_)
|
||||||
|
import Web.Event.Event (Event, preventDefault)
|
||||||
|
import Web.HTML (window)
|
||||||
|
import Web.HTML.Location (setHref)
|
||||||
|
|
||||||
|
data BQuery a
|
||||||
|
= BEditField EditField a
|
||||||
|
| BEditSubmit Event a
|
||||||
|
| BDeleteAsk Boolean a
|
||||||
|
| BDestroy a
|
||||||
|
|
||||||
|
data EditField
|
||||||
|
= Eurl String
|
||||||
|
| Etitle String
|
||||||
|
| Edescription String
|
||||||
|
| Etags String
|
||||||
|
| Eprivate Boolean
|
||||||
|
| Etoread Boolean
|
||||||
|
|
||||||
|
type BState =
|
||||||
|
{ bm :: Bookmark
|
||||||
|
, edit_bm :: Bookmark
|
||||||
|
, deleteAsk :: Boolean
|
||||||
|
, destroyed :: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
_bm :: Lens' BState Bookmark
|
||||||
|
_bm = lens _.bm (_ { bm = _ })
|
||||||
|
|
||||||
|
_edit_bm :: Lens' BState Bookmark
|
||||||
|
_edit_bm = lens _.edit_bm (_ { edit_bm = _ })
|
||||||
|
|
||||||
|
addbmark :: Bookmark -> H.Component HTML BQuery Unit Unit Aff
|
||||||
|
addbmark b' =
|
||||||
|
H.component
|
||||||
|
{ initialState: const (mkState b')
|
||||||
|
, render
|
||||||
|
, eval
|
||||||
|
, receiver: const Nothing
|
||||||
|
}
|
||||||
|
where
|
||||||
|
app = app' unit
|
||||||
|
|
||||||
|
mkState b =
|
||||||
|
{ bm: b
|
||||||
|
, edit_bm: b
|
||||||
|
, deleteAsk: false
|
||||||
|
, destroyed: false
|
||||||
|
}
|
||||||
|
|
||||||
|
render :: BState -> H.ComponentHTML BQuery
|
||||||
|
render s@{ bm, edit_bm } =
|
||||||
|
div_ [ if not s.destroyed then display_edit else display_destroyed ]
|
||||||
|
where
|
||||||
|
display_edit =
|
||||||
|
form [ onSubmit (HE.input BEditSubmit) ]
|
||||||
|
[ table [ class_ "w-100" ]
|
||||||
|
[ tbody_
|
||||||
|
[ tr_
|
||||||
|
[ td [ class_ "w1" ] [ ]
|
||||||
|
, td_ $ guard (bm.bid > 0) [ display_exists ]
|
||||||
|
]
|
||||||
|
, tr_
|
||||||
|
[ td_ [ label [ for "url" ] [ text "URL" ] ]
|
||||||
|
, td_ [ input [ type_ InputUrl , id_ "url", class_ "w-100 mv1" , required true, name "url", autofocus (null bm.url)
|
||||||
|
, value (edit_bm.url) , onValueChange (editField Eurl)] ]
|
||||||
|
]
|
||||||
|
, tr_
|
||||||
|
[ td_ [ label [ for "title" ] [ text "title" ] ]
|
||||||
|
, td_ [ input [ type_ InputText , id_ "title", class_ "w-100 mv1" , name "title"
|
||||||
|
, value (edit_bm.title) , onValueChange (editField Etitle)] ]
|
||||||
|
]
|
||||||
|
, tr_
|
||||||
|
[ td_ [ label [ for "description" ] [ text "description" ] ]
|
||||||
|
, td_ [ textarea [ class_ "w-100 mt1 mid-gray" , id_ "description", name "description", rows 4
|
||||||
|
, value (edit_bm.description) , onValueChange (editField Edescription)] ]
|
||||||
|
]
|
||||||
|
, tr_
|
||||||
|
[ td_ [ label [ for "tags" ] [ text "tags" ] ]
|
||||||
|
, td_ [ input [ type_ InputText , id_ "tags", class_ "w-100 mv1" , name "tags", autocomplete false, attr "autocapitalize" "off", autofocus (not $ null bm.url)
|
||||||
|
, value (edit_bm.tags) , onValueChange (editField Etags)] ]
|
||||||
|
]
|
||||||
|
, tr_
|
||||||
|
[ td_ [ label [ for "private" ] [ text "private" ] ]
|
||||||
|
, td_ [ input [ type_ InputCheckbox , id_ "private", class_ "private pointer" , name "private"
|
||||||
|
, checked (edit_bm.private) , onChecked (editField Eprivate)] ]
|
||||||
|
]
|
||||||
|
, tr_
|
||||||
|
[ td_ [ label [ for "toread" ] [ text "read later" ] ]
|
||||||
|
, td_ [ input [ type_ InputCheckbox , id_ "toread", class_ "toread pointer" , name "toread"
|
||||||
|
, checked (edit_bm.toread) , onChecked (editField Etoread)] ]
|
||||||
|
]
|
||||||
|
, tr_
|
||||||
|
[ td_ [ ]
|
||||||
|
, td_ [ input [ type_ InputSubmit , class_ "ph3 pv2 input-reset ba b--navy bg-transparent pointer f6 dib mt1 dim"
|
||||||
|
, value (if bm.bid > 0 then "update bookmark" else "add bookmark") ] ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
display_exists =
|
||||||
|
div [ class_ "alert" ]
|
||||||
|
[ text "previously saved "
|
||||||
|
, span [ class_ "link f7 dib gray pr3" , title (maybe bm.time snd mmoment) ]
|
||||||
|
[ text (maybe " " fst mmoment) ]
|
||||||
|
, div [ class_ "edit_links dib ml1" ]
|
||||||
|
[ div [ class_ "delete_link di" ]
|
||||||
|
[ button ([ type_ ButtonButton, onClick (HE.input_ (BDeleteAsk true)), class_ "delete" ] <> guard s.deleteAsk [ attr "hidden" "hidden" ]) [ text "delete" ]
|
||||||
|
, span ([ class_ "confirm red" ] <> guard (not s.deleteAsk) [ attr "hidden" "hidden" ])
|
||||||
|
[ button [ type_ ButtonButton, onClick (HE.input_ (BDeleteAsk false))] [ text "cancel / " ]
|
||||||
|
, button [ type_ ButtonButton, onClick (HE.input_ BDestroy), class_ "red" ] [ text "destroy" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
display_destroyed = p [ class_ "red"] [text "you killed this bookmark"]
|
||||||
|
|
||||||
|
editField :: forall a. (a -> EditField) -> a -> Maybe (BQuery Unit)
|
||||||
|
editField f = HE.input BEditField <<< f
|
||||||
|
mmoment = mmoment8601 bm.time
|
||||||
|
toTextarea =
|
||||||
|
drop 1
|
||||||
|
<<< foldMap (\x -> [br_, text x])
|
||||||
|
<<< S.split (Pattern "\n")
|
||||||
|
|
||||||
|
eval :: BQuery ~> H.ComponentDSL BState BQuery Unit Aff
|
||||||
|
eval (BDeleteAsk e next) = do
|
||||||
|
H.modify_ (_ { deleteAsk = e })
|
||||||
|
pure next
|
||||||
|
eval (BDestroy next) = do
|
||||||
|
bid <- H.gets _.bm.bid
|
||||||
|
void $ H.liftAff (destroy bid)
|
||||||
|
H.modify_ (_ { destroyed = true })
|
||||||
|
pure next
|
||||||
|
eval (BEditField f next) = do
|
||||||
|
_edit_bm %= case f of
|
||||||
|
Eurl e -> _ { url = e }
|
||||||
|
Etitle e -> _ { title = e }
|
||||||
|
Edescription e -> _ { description = e }
|
||||||
|
Etags e -> _ { tags = e }
|
||||||
|
Eprivate e -> _ { private = e }
|
||||||
|
Etoread e -> _ { toread = e }
|
||||||
|
pure next
|
||||||
|
eval (BEditSubmit e next) = do
|
||||||
|
H.liftEffect (preventDefault e)
|
||||||
|
edit_bm <- use _edit_bm
|
||||||
|
void $ H.liftAff (editBookmark edit_bm)
|
||||||
|
_bm .= edit_bm
|
||||||
|
loc <- liftEffect _loc
|
||||||
|
win <- liftEffect window
|
||||||
|
qs <- liftEffect _curQuerystring
|
||||||
|
case _lookupQueryStringValue qs "next" of
|
||||||
|
Just n -> liftEffect (setHref n loc)
|
||||||
|
_ -> liftEffect (closeWindow win)
|
||||||
|
pure next
|
48
purs/src/Component/BList.purs
Normal file
48
purs/src/Component/BList.purs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
module Component.BList where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Component.BMark (BMessage(..), BQuery, bmark)
|
||||||
|
import Model (Bookmark, BookmarkId)
|
||||||
|
|
||||||
|
import Data.Array (filter)
|
||||||
|
import Data.Maybe (Maybe(..))
|
||||||
|
import Effect.Aff (Aff)
|
||||||
|
import Halogen as H
|
||||||
|
import Halogen.HTML as HH
|
||||||
|
import Halogen.HTML.Events as HE
|
||||||
|
|
||||||
|
type BSlot = BookmarkId
|
||||||
|
|
||||||
|
data LQuery a =
|
||||||
|
HandleBMessage BSlot BMessage a
|
||||||
|
|
||||||
|
blist :: Array Bookmark -> H.Component HH.HTML LQuery Unit Void Aff
|
||||||
|
blist st =
|
||||||
|
H.parentComponent
|
||||||
|
{ initialState: const st
|
||||||
|
, render
|
||||||
|
, eval
|
||||||
|
, receiver: const Nothing
|
||||||
|
}
|
||||||
|
where
|
||||||
|
|
||||||
|
render :: Array Bookmark -> H.ParentHTML LQuery BQuery BSlot Aff
|
||||||
|
render bms =
|
||||||
|
HH.div_ (map renderBookmark bms)
|
||||||
|
where
|
||||||
|
renderBookmark :: Bookmark -> H.ParentHTML LQuery BQuery BSlot Aff
|
||||||
|
renderBookmark b =
|
||||||
|
HH.slot
|
||||||
|
b.bid
|
||||||
|
(bmark b)
|
||||||
|
unit
|
||||||
|
(HE.input (HandleBMessage b.bid))
|
||||||
|
|
||||||
|
eval :: LQuery ~> H.ParentDSL (Array Bookmark) LQuery BQuery BSlot Void Aff
|
||||||
|
eval (HandleBMessage p BNotifyRemove next) = do
|
||||||
|
H.modify_ (removeBookmark p)
|
||||||
|
pure next
|
||||||
|
where
|
||||||
|
removeBookmark :: BookmarkId -> Array Bookmark -> Array Bookmark
|
||||||
|
removeBookmark bookmarkId = filter (\b -> b.bid /= bookmarkId)
|
247
purs/src/Component/BMark.purs
Normal file
247
purs/src/Component/BMark.purs
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
module Component.BMark where
|
||||||
|
|
||||||
|
import Prelude hiding (div)
|
||||||
|
|
||||||
|
import App (StarAction(..), destroy, editBookmark, markRead, toggleStar)
|
||||||
|
import Data.Array (drop, foldMap)
|
||||||
|
import Data.Lens (Lens', lens, use, (%=), (.=))
|
||||||
|
import Data.Maybe (Maybe(..), fromMaybe, isJust, maybe)
|
||||||
|
import Data.Monoid (guard)
|
||||||
|
import Data.Nullable (toMaybe)
|
||||||
|
import Data.String (null, split, take) as S
|
||||||
|
import Data.String.Pattern (Pattern(..))
|
||||||
|
import Data.Tuple (fst, snd)
|
||||||
|
import Effect.Aff (Aff)
|
||||||
|
import Globals (app', mmoment8601)
|
||||||
|
import Halogen as H
|
||||||
|
import Halogen.HTML (HTML, a, br_, button, div, div_, form, input, label, span, text, textarea)
|
||||||
|
import Halogen.HTML.Events (onSubmit, onValueChange, onChecked, onClick)
|
||||||
|
import Halogen.HTML.Events as HE
|
||||||
|
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, checked, for, href, id_, name, required, rows, target, title, type_, value)
|
||||||
|
import Model (Bookmark)
|
||||||
|
import Util (class_, attr, fromNullableStr)
|
||||||
|
import Web.Event.Event (Event, preventDefault)
|
||||||
|
|
||||||
|
-- | UI Events
|
||||||
|
data BQuery a
|
||||||
|
= BStar Boolean a
|
||||||
|
| BDeleteAsk Boolean a
|
||||||
|
| BDestroy a
|
||||||
|
| BEdit Boolean a
|
||||||
|
| BEditField EditField a
|
||||||
|
| BEditSubmit Event a
|
||||||
|
| BMarkRead a
|
||||||
|
|
||||||
|
-- | FormField Edits
|
||||||
|
data EditField
|
||||||
|
= Eurl String
|
||||||
|
| Etitle String
|
||||||
|
| Edescription String
|
||||||
|
| Etags String
|
||||||
|
| Eprivate Boolean
|
||||||
|
| Etoread Boolean
|
||||||
|
|
||||||
|
-- | Messages to parent
|
||||||
|
data BMessage
|
||||||
|
= BNotifyRemove
|
||||||
|
|
||||||
|
type BState =
|
||||||
|
{ bm :: Bookmark
|
||||||
|
, edit_bm :: Bookmark
|
||||||
|
, deleteAsk:: Boolean
|
||||||
|
, edit :: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
_bm :: Lens' BState Bookmark
|
||||||
|
_bm = lens _.bm (_ { bm = _ })
|
||||||
|
|
||||||
|
_edit_bm :: Lens' BState Bookmark
|
||||||
|
_edit_bm = lens _.edit_bm (_ { edit_bm = _ })
|
||||||
|
|
||||||
|
_edit :: Lens' BState Boolean
|
||||||
|
_edit = lens _.edit (_ { edit = _ })
|
||||||
|
|
||||||
|
bmark :: Bookmark -> H.Component HTML BQuery Unit BMessage Aff
|
||||||
|
bmark b' =
|
||||||
|
H.component
|
||||||
|
{ initialState: const (mkState b')
|
||||||
|
, render
|
||||||
|
, eval
|
||||||
|
, receiver: const Nothing
|
||||||
|
}
|
||||||
|
where
|
||||||
|
app = app' unit
|
||||||
|
|
||||||
|
mkState b =
|
||||||
|
{ bm: b
|
||||||
|
, edit_bm: b
|
||||||
|
, deleteAsk: false
|
||||||
|
, edit: false
|
||||||
|
}
|
||||||
|
|
||||||
|
render :: BState -> H.ComponentHTML BQuery
|
||||||
|
render s@{ bm, edit_bm } =
|
||||||
|
div [ id_ (show bm.bid) , class_ ("bookmark w-100 mw7 pa1 mb3" <> guard bm.private " private")] $
|
||||||
|
star <>
|
||||||
|
if s.edit
|
||||||
|
then display_edit
|
||||||
|
else display
|
||||||
|
where
|
||||||
|
|
||||||
|
star =
|
||||||
|
guard app.dat.isowner
|
||||||
|
[ div [ class_ ("star fl pointer" <> guard bm.selected " selected") ]
|
||||||
|
[ button [ class_ "moon-gray", onClick (HE.input_ (BStar (not bm.selected))) ] [ text "✭" ] ]
|
||||||
|
]
|
||||||
|
|
||||||
|
display =
|
||||||
|
[ div [ class_ "display" ] $
|
||||||
|
[ a [ href bm.url, target "_blank", class_ ("link f5 lh-title" <> guard bm.toread " unread")]
|
||||||
|
[ text $ if S.null bm.title then "[no title]" else bm.title ]
|
||||||
|
, br_
|
||||||
|
, a [ href bm.url , class_ "link f7 gray hover-blue" ] [ text bm.url ]
|
||||||
|
, a [ href (fromMaybe ("http://archive.is/" <> bm.url) (toMaybe bm.archiveUrl))
|
||||||
|
, class_ ("link f7 gray hover-blue ml2" <> (guard (isJust (toMaybe bm.archiveUrl)) " green"))
|
||||||
|
, target "_blank", title "archive link"]
|
||||||
|
[ if isJust (toMaybe bm.archiveUrl) then text "☑" else text "☐" ]
|
||||||
|
, br_
|
||||||
|
--
|
||||||
|
, div [ class_ "description mt1 mid-gray" ] (toTextarea bm.description)
|
||||||
|
, div [ class_ "tags" ] $
|
||||||
|
guard (not (S.null bm.tags))
|
||||||
|
map (\tag -> a [ class_ ("link tag mr1" <> guard (S.take 1 tag == ".") " private")
|
||||||
|
, href (linkToFilterTag tag) ]
|
||||||
|
[ text tag ])
|
||||||
|
(S.split (Pattern " ") bm.tags)
|
||||||
|
, a [ class_ "link f7 dib gray w4", title (maybe bm.time snd mmoment) , href (linkToFilterSingle bm.slug) ]
|
||||||
|
[ text (maybe " " fst mmoment) ]
|
||||||
|
]
|
||||||
|
<> links
|
||||||
|
]
|
||||||
|
|
||||||
|
display_edit =
|
||||||
|
[ div [ class_ "edit_bookmark_form pa2 pt0 bg-white" ] $
|
||||||
|
[ form [ onSubmit (HE.input BEditSubmit) ]
|
||||||
|
[ div_ [ text "url" ]
|
||||||
|
, input [ type_ InputUrl , class_ "url w-100 mb2 pt1 f7 edit_form_input" , required true , name "url"
|
||||||
|
, value (edit_bm.url) , onValueChange (editField Eurl) ]
|
||||||
|
, br_
|
||||||
|
, div_ [ text "title" ]
|
||||||
|
, input [ type_ InputText , class_ "title w-100 mb2 pt1 f7 edit_form_input" , name "title"
|
||||||
|
, value (edit_bm.title) , onValueChange (editField Etitle) ]
|
||||||
|
, br_
|
||||||
|
, div_ [ text "description" ]
|
||||||
|
, textarea [ class_ "description w-100 mb1 pt1 f7 edit_form_input" , name "description", rows 5
|
||||||
|
, value (edit_bm.description) , onValueChange (editField Edescription) ]
|
||||||
|
, br_
|
||||||
|
, div [ id_ "tags_input_box"]
|
||||||
|
[ div_ [ text "tags" ]
|
||||||
|
, input [ type_ InputText , class_ "tags w-100 mb1 pt1 f7 edit_form_input" , name "tags"
|
||||||
|
, autocomplete false, attr "autocapitalize" "off"
|
||||||
|
, value (edit_bm.tags) , onValueChange (editField Etags) ]
|
||||||
|
, br_
|
||||||
|
]
|
||||||
|
, div [ class_ "edit_form_checkboxes mv3"]
|
||||||
|
[ input [ type_ InputCheckbox , class_ "private pointer" , id_ "edit_private", name "private"
|
||||||
|
, checked (edit_bm.private) , onChecked (editField Eprivate) ]
|
||||||
|
, text " "
|
||||||
|
, label [ for "edit_private" , class_ "mr2" ] [ text "private" ]
|
||||||
|
, text " "
|
||||||
|
, input [ type_ InputCheckbox , class_ "toread pointer" , id_ "edit_toread", name "toread"
|
||||||
|
, checked (edit_bm.toread) , onChecked (editField Etoread) ]
|
||||||
|
, text " "
|
||||||
|
, label [ for "edit_toread" ] [ text "to-read" ]
|
||||||
|
, br_
|
||||||
|
]
|
||||||
|
, input [ type_ InputSubmit , class_ "mr1 pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "save" ]
|
||||||
|
, text " "
|
||||||
|
, input [ type_ InputReset , class_ "pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "cancel"
|
||||||
|
, onClick (HE.input_ (BEdit false)) ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
links =
|
||||||
|
guard app.dat.isowner
|
||||||
|
[ div [ class_ "edit_links di" ]
|
||||||
|
[ button [ type_ ButtonButton, onClick (HE.input_ (BEdit true)), class_ "edit light-silver hover-blue" ] [ text "edit " ]
|
||||||
|
, div [ class_ "delete_link di" ]
|
||||||
|
[ button [ type_ ButtonButton, onClick (HE.input_ (BDeleteAsk true)), class_ ("delete light-silver hover-blue" <> guard s.deleteAsk " dn") ] [ text "delete" ]
|
||||||
|
, span ([ class_ ("confirm red" <> guard (not s.deleteAsk) " dn") ] )
|
||||||
|
[ button [ type_ ButtonButton, onClick (HE.input_ (BDeleteAsk false))] [ text "cancel / " ]
|
||||||
|
, button [ type_ ButtonButton, onClick (HE.input_ BDestroy), class_ "red" ] [ text "destroy" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class_ "read di" ] $
|
||||||
|
guard bm.toread
|
||||||
|
[ text " "
|
||||||
|
, button [ onClick (HE.input_ BMarkRead), class_ "mark_read" ] [ text "mark as read"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
editField :: forall a. (a -> EditField) -> a -> Maybe (BQuery Unit)
|
||||||
|
editField f = HE.input BEditField <<< f
|
||||||
|
linkToFilterSingle slug = fromNullableStr app.userR <> "/b:" <> slug
|
||||||
|
linkToFilterTag tag = fromNullableStr app.userR <> "/t:" <> tag
|
||||||
|
mmoment = mmoment8601 bm.time
|
||||||
|
toTextarea input =
|
||||||
|
S.split (Pattern "\n") input
|
||||||
|
# foldMap (\x -> [br_, text x])
|
||||||
|
# drop 1
|
||||||
|
|
||||||
|
eval :: BQuery ~> H.ComponentDSL BState BQuery BMessage Aff
|
||||||
|
|
||||||
|
-- | Star
|
||||||
|
eval (BStar e next) = do
|
||||||
|
bm <- use _bm
|
||||||
|
H.liftAff (toggleStar bm.bid (if e then Star else UnStar))
|
||||||
|
_bm %= _ { selected = e }
|
||||||
|
_edit_bm %= _ { selected = e }
|
||||||
|
pure next
|
||||||
|
|
||||||
|
-- | Delete
|
||||||
|
eval (BDeleteAsk e next) = do
|
||||||
|
H.modify_ (_ { deleteAsk = e })
|
||||||
|
pure next
|
||||||
|
|
||||||
|
-- | Destroy
|
||||||
|
eval (BDestroy next) = do
|
||||||
|
bm <- use _bm
|
||||||
|
void $ H.liftAff (destroy bm.bid)
|
||||||
|
H.raise BNotifyRemove
|
||||||
|
pure next
|
||||||
|
|
||||||
|
-- | Mark Read
|
||||||
|
eval (BMarkRead next) = do
|
||||||
|
bm <- use _bm
|
||||||
|
void (H.liftAff (markRead bm.bid))
|
||||||
|
_bm %= _ { toread = false }
|
||||||
|
pure next
|
||||||
|
|
||||||
|
-- | Start/Stop Editing
|
||||||
|
eval (BEdit e next) = do
|
||||||
|
bm <- use _bm
|
||||||
|
_edit_bm .= bm
|
||||||
|
_edit .= e
|
||||||
|
pure next
|
||||||
|
|
||||||
|
-- | Update Form Field
|
||||||
|
eval (BEditField f next) = do
|
||||||
|
_edit_bm %= case f of
|
||||||
|
Eurl e -> _ { url = e }
|
||||||
|
Etitle e -> _ { title = e }
|
||||||
|
Edescription e -> _ { description = e }
|
||||||
|
Etags e -> _ { tags = e }
|
||||||
|
Eprivate e -> _ { private = e }
|
||||||
|
Etoread e -> _ { toread = e }
|
||||||
|
pure next
|
||||||
|
|
||||||
|
-- | Submit
|
||||||
|
eval (BEditSubmit e next) = do
|
||||||
|
H.liftEffect (preventDefault e)
|
||||||
|
edit_bm <- use _edit_bm
|
||||||
|
void $ H.liftAff (editBookmark edit_bm)
|
||||||
|
_bm .= edit_bm
|
||||||
|
_edit .= false
|
||||||
|
pure next
|
15
purs/src/Component/Markdown.purs
Normal file
15
purs/src/Component/Markdown.purs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module Component.Markdown (component, MInput, MQuery, MOutput, module RHExt) where
|
||||||
|
|
||||||
|
import Component.RawHtml as RH
|
||||||
|
import Component.RawHtml (Query(Receive)) as RHExt
|
||||||
|
import Effect.Aff (Aff)
|
||||||
|
import Foreign.Marked (marked)
|
||||||
|
import Halogen as H
|
||||||
|
import Halogen.HTML as HH
|
||||||
|
|
||||||
|
type MInput = String
|
||||||
|
type MQuery = RH.Query String
|
||||||
|
type MOutput = RH.Output
|
||||||
|
|
||||||
|
component :: H.Component HH.HTML MQuery MInput MOutput Aff
|
||||||
|
component = RH.mkComponent marked
|
75
purs/src/Component/NList.purs
Normal file
75
purs/src/Component/NList.purs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
module Component.NList where
|
||||||
|
|
||||||
|
import Prelude hiding (div)
|
||||||
|
|
||||||
|
import Data.Array (drop, foldMap)
|
||||||
|
import Data.Maybe (Maybe(..), maybe)
|
||||||
|
import Data.String (null, split, take) as S
|
||||||
|
import Data.String.Pattern (Pattern(..))
|
||||||
|
import Data.Tuple (fst, snd)
|
||||||
|
import Effect.Aff (Aff)
|
||||||
|
import Globals (app', mmoment8601)
|
||||||
|
import Halogen as H
|
||||||
|
import Halogen.HTML (a, br_, div, text)
|
||||||
|
import Halogen.HTML as HH
|
||||||
|
import Halogen.HTML.Properties (href, id_, title)
|
||||||
|
import Model (Note, NoteSlug)
|
||||||
|
import Util (class_, fromNullableStr)
|
||||||
|
|
||||||
|
data NLQuery a
|
||||||
|
= NLNop a
|
||||||
|
|
||||||
|
type NLSlot = NoteSlug
|
||||||
|
|
||||||
|
type NLState =
|
||||||
|
{ notes :: Array Note
|
||||||
|
, cur :: Maybe NLSlot
|
||||||
|
, deleteAsk:: Boolean
|
||||||
|
, edit :: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
nlist :: Array Note -> H.Component HH.HTML NLQuery Unit Void Aff
|
||||||
|
nlist st' =
|
||||||
|
H.component
|
||||||
|
{ initialState: const (mkState st')
|
||||||
|
, render
|
||||||
|
, eval
|
||||||
|
, receiver: const Nothing
|
||||||
|
}
|
||||||
|
where
|
||||||
|
app = app' unit
|
||||||
|
|
||||||
|
mkState notes' =
|
||||||
|
{ notes: notes'
|
||||||
|
, cur: Nothing
|
||||||
|
, deleteAsk: false
|
||||||
|
, edit: false
|
||||||
|
}
|
||||||
|
|
||||||
|
render :: NLState -> H.ComponentHTML NLQuery
|
||||||
|
render st@{ notes } =
|
||||||
|
HH.div_ (map renderNote notes)
|
||||||
|
where
|
||||||
|
renderNote :: Note -> H.ComponentHTML NLQuery
|
||||||
|
renderNote bm =
|
||||||
|
div [ id_ (show bm.id) , class_ ("note w-100 mw7 pa1 mb2")] $
|
||||||
|
[ div [ class_ "display" ] $
|
||||||
|
[ a [ href (linkToFilterSingle bm.slug), class_ ("link f5 lh-title")]
|
||||||
|
[ text $ if S.null bm.title then "[no title]" else bm.title ]
|
||||||
|
, br_
|
||||||
|
, div [ class_ "description mt1 mid-gray" ] (toTextarea (S.take 200 bm.text))
|
||||||
|
, a [ class_ "link f7 dib gray w4", title (maybe bm.created snd (mmoment bm)) , href (linkToFilterSingle bm.slug) ]
|
||||||
|
[ text (maybe " " fst (mmoment bm)) ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
mmoment bm = mmoment8601 bm.created
|
||||||
|
linkToFilterSingle slug = fromNullableStr app.userR <> "/notes/" <> slug
|
||||||
|
toTextarea input =
|
||||||
|
S.split (Pattern "\n") input
|
||||||
|
# foldMap (\x -> [br_, text x])
|
||||||
|
# drop 1
|
||||||
|
|
||||||
|
eval :: NLQuery ~> H.ComponentDSL NLState NLQuery Void Aff
|
||||||
|
eval (NLNop next) = pure next
|
197
purs/src/Component/NNote.purs
Normal file
197
purs/src/Component/NNote.purs
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
module Component.NNote where
|
||||||
|
|
||||||
|
import Prelude hiding (div)
|
||||||
|
|
||||||
|
import App (destroyNote, editNote)
|
||||||
|
import Component.Markdown as Markdown
|
||||||
|
import Data.Array (drop, foldMap)
|
||||||
|
import Data.Either (Either(..))
|
||||||
|
import Data.Lens (Lens', lens, use, (%=), (.=))
|
||||||
|
import Data.Maybe (Maybe(..), maybe)
|
||||||
|
import Data.Monoid (guard)
|
||||||
|
import Data.String (null, split) as S
|
||||||
|
import Data.String.Pattern (Pattern(..))
|
||||||
|
import Data.Tuple (fst, snd)
|
||||||
|
import Effect.Aff (Aff)
|
||||||
|
import Effect.Class (liftEffect)
|
||||||
|
import Globals (app', mmoment8601)
|
||||||
|
import Halogen as H
|
||||||
|
import Halogen.HTML (br_, button, div, form, input, label, p, span, text, textarea)
|
||||||
|
import Halogen.HTML as HH
|
||||||
|
import Halogen.HTML.Events (onChecked, onClick, onSubmit, onValueChange)
|
||||||
|
import Halogen.HTML.Events as HE
|
||||||
|
import Halogen.HTML.Properties (ButtonType(..), InputType(..), checked, for, id_, name, rows, title, type_, value)
|
||||||
|
import Model (Note)
|
||||||
|
import Util (_loc, class_, fromNullableStr)
|
||||||
|
import Web.Event.Event (Event, preventDefault)
|
||||||
|
import Web.HTML.Location (setHref)
|
||||||
|
|
||||||
|
data NQuery a
|
||||||
|
= NNop a
|
||||||
|
| NEditField EditField a
|
||||||
|
| NEditSubmit Event a
|
||||||
|
| NEdit Boolean a
|
||||||
|
| NDeleteAsk Boolean a
|
||||||
|
| NDestroy a
|
||||||
|
|
||||||
|
type NState =
|
||||||
|
{ note :: Note
|
||||||
|
, edit_note :: Note
|
||||||
|
, deleteAsk :: Boolean
|
||||||
|
, edit :: Boolean
|
||||||
|
, destroyed :: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
_note :: Lens' NState Note
|
||||||
|
_note = lens _.note (_ { note = _ })
|
||||||
|
|
||||||
|
_edit_note :: Lens' NState Note
|
||||||
|
_edit_note = lens _.edit_note (_ { edit_note = _ })
|
||||||
|
|
||||||
|
_edit :: Lens' NState Boolean
|
||||||
|
_edit = lens _.edit (_ { edit = _ })
|
||||||
|
|
||||||
|
-- | FormField Edits
|
||||||
|
data EditField
|
||||||
|
= Etitle String
|
||||||
|
| Etext String
|
||||||
|
| EisMarkdown Boolean
|
||||||
|
|
||||||
|
type NChildQuery = Markdown.MQuery
|
||||||
|
|
||||||
|
nnote :: Note -> H.Component HH.HTML NQuery Unit Void Aff
|
||||||
|
nnote st' =
|
||||||
|
H.parentComponent
|
||||||
|
{ initialState: const (mkState st')
|
||||||
|
, render
|
||||||
|
, eval
|
||||||
|
, receiver: const Nothing
|
||||||
|
}
|
||||||
|
where
|
||||||
|
app = app' unit
|
||||||
|
|
||||||
|
mkState note' =
|
||||||
|
{ note: note'
|
||||||
|
, edit_note: note'
|
||||||
|
, deleteAsk: false
|
||||||
|
, edit: note'.id <= 0
|
||||||
|
, destroyed: false
|
||||||
|
}
|
||||||
|
|
||||||
|
render :: NState -> H.ParentHTML NQuery NChildQuery Unit Aff
|
||||||
|
render st@{ note, edit_note } =
|
||||||
|
if st.destroyed
|
||||||
|
then display_destroyed
|
||||||
|
else
|
||||||
|
if st.edit
|
||||||
|
then renderNote_edit
|
||||||
|
else renderNote
|
||||||
|
where
|
||||||
|
|
||||||
|
renderNote =
|
||||||
|
div [ id_ (show note.id) , class_ ("note w-100 mw7 pa1 mb2")] $
|
||||||
|
[ div [ class_ "display" ] $
|
||||||
|
[ div [ class_ ("link f5 lh-title")]
|
||||||
|
[ text $ if S.null note.title then "[no title]" else note.title ]
|
||||||
|
, br_
|
||||||
|
, if note.isMarkdown
|
||||||
|
then div [ class_ "description mt1" ] [ HH.slot unit Markdown.component note.text absurd ]
|
||||||
|
else div [ class_ "description mt1 mid-gray" ] (toTextarea note.text)
|
||||||
|
, div [ class_ "link f7 dib gray w4", title (maybe note.created snd (mmoment note)) ]
|
||||||
|
[ text (maybe " " fst (mmoment note)) ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
<> -- | Render Action Links
|
||||||
|
[ div [ class_ "edit_links db mt3" ]
|
||||||
|
[ button [ type_ ButtonButton, onClick (HE.input_ (NEdit true)), class_ "edit light-silver hover-blue" ] [ text "edit " ]
|
||||||
|
, div [ class_ "delete_link di" ]
|
||||||
|
[ button [ type_ ButtonButton, onClick (HE.input_ (NDeleteAsk true)), class_ ("delete light-silver hover-blue" <> guard st.deleteAsk " dn") ] [ text "delete" ]
|
||||||
|
, span ([ class_ ("confirm red" <> guard (not st.deleteAsk) " dn") ] )
|
||||||
|
[ button [ type_ ButtonButton, onClick (HE.input_ (NDeleteAsk false))] [ text "cancel / " ]
|
||||||
|
, button [ type_ ButtonButton, onClick (HE.input_ NDestroy), class_ "red" ] [ text "destroy" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
renderNote_edit =
|
||||||
|
form [ onSubmit (HE.input NEditSubmit) ]
|
||||||
|
[ p [ class_ "mt2 mb1"] [ text "title:" ]
|
||||||
|
, input [ type_ InputText , class_ "title w-100 mb1 pt1 f7 edit_form_input" , name "title"
|
||||||
|
, value (edit_note.title) , onValueChange (editField Etitle)
|
||||||
|
]
|
||||||
|
, br_
|
||||||
|
, p [ class_ "mt2 mb1"] [ text "description:" ]
|
||||||
|
, textarea [ class_ "description w-100 mb1 pt1 f7 edit_form_input" , name "text", rows 30
|
||||||
|
, value (edit_note.text) , onValueChange (editField Etext)
|
||||||
|
]
|
||||||
|
, div [ class_ "edit_form_checkboxes mb3"]
|
||||||
|
[ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id_ "edit_ismarkdown", name "ismarkdown"
|
||||||
|
, checked (edit_note.isMarkdown) , onChecked (editField EisMarkdown) ]
|
||||||
|
, text " "
|
||||||
|
, label [ for "edit_ismarkdown" , class_ "mr2" ] [ text "use markdown?" ]
|
||||||
|
, br_
|
||||||
|
]
|
||||||
|
, input [ type_ InputSubmit , class_ "mr1 pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "save" ]
|
||||||
|
, text " "
|
||||||
|
, input [ type_ InputReset , class_ "pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "cancel"
|
||||||
|
, onClick (HE.input_ (NEdit false))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
display_destroyed = p [ class_ "red"] [text "you killed this note"]
|
||||||
|
|
||||||
|
mmoment n = mmoment8601 n.created
|
||||||
|
editField :: forall a. (a -> EditField) -> a -> Maybe (NQuery Unit)
|
||||||
|
editField f = HE.input NEditField <<< f
|
||||||
|
toTextarea input =
|
||||||
|
S.split (Pattern "\n") input
|
||||||
|
# foldMap (\x -> [br_, text x])
|
||||||
|
# drop 1
|
||||||
|
|
||||||
|
|
||||||
|
eval :: NQuery ~> H.ParentDSL NState NQuery NChildQuery Unit Void Aff
|
||||||
|
eval (NNop next) = pure next
|
||||||
|
|
||||||
|
-- | EditField
|
||||||
|
eval (NEditField f next) = do
|
||||||
|
_edit_note %= case f of
|
||||||
|
Etitle e -> _ { title = e }
|
||||||
|
Etext e -> _ { text = e }
|
||||||
|
EisMarkdown e -> _ { isMarkdown = e }
|
||||||
|
pure next
|
||||||
|
|
||||||
|
-- | Delete
|
||||||
|
eval (NDeleteAsk e next) = do
|
||||||
|
H.modify_ (_ { deleteAsk = e })
|
||||||
|
pure next
|
||||||
|
|
||||||
|
-- | Destroy
|
||||||
|
eval (NDestroy next) = do
|
||||||
|
note <- use _note
|
||||||
|
void $ H.liftAff (destroyNote note.id)
|
||||||
|
H.modify_ (_ { destroyed = true })
|
||||||
|
pure next
|
||||||
|
|
||||||
|
-- | Start/Stop Editing
|
||||||
|
eval (NEdit e next) = do
|
||||||
|
note <- use _note
|
||||||
|
_edit_note .= note
|
||||||
|
_edit .= e
|
||||||
|
pure next
|
||||||
|
|
||||||
|
-- | Submit
|
||||||
|
eval (NEditSubmit e next) = do
|
||||||
|
H.liftEffect (preventDefault e)
|
||||||
|
edit_note <- use _edit_note
|
||||||
|
res <- H.liftAff (editNote edit_note)
|
||||||
|
case res.body of
|
||||||
|
Left err -> pure next
|
||||||
|
Right r -> do
|
||||||
|
if (edit_note.id == 0)
|
||||||
|
then do
|
||||||
|
liftEffect (setHref (fromNullableStr app.noteR) =<< _loc)
|
||||||
|
else do
|
||||||
|
_note .= edit_note
|
||||||
|
_edit .= false
|
||||||
|
pure next
|
8
purs/src/Component/RawHtml.js
Normal file
8
purs/src/Component/RawHtml.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// use at your own risk!
|
||||||
|
exports.unsafeSetInnerHTML = function(element) {
|
||||||
|
return function(html) {
|
||||||
|
return function() {
|
||||||
|
element.innerHTML = html;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
62
purs/src/Component/RawHtml.purs
Normal file
62
purs/src/Component/RawHtml.purs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
module Component.RawHtml where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Data.Foldable (for_)
|
||||||
|
import Data.Maybe (Maybe(..))
|
||||||
|
import Effect (Effect)
|
||||||
|
import Effect.Aff (Aff)
|
||||||
|
import Globals (RawHTML(..))
|
||||||
|
import Halogen as H
|
||||||
|
import Halogen.HTML as HH
|
||||||
|
import Halogen.HTML.Events as HE
|
||||||
|
import Halogen.HTML.Properties as HP
|
||||||
|
import Web.HTML (HTMLElement)
|
||||||
|
|
||||||
|
foreign import unsafeSetInnerHTML :: HTMLElement -> RawHTML -> Effect Unit
|
||||||
|
|
||||||
|
data Query i a
|
||||||
|
= SetInnerHTML a
|
||||||
|
| Receive (Input i) a
|
||||||
|
|
||||||
|
type Input i = i
|
||||||
|
|
||||||
|
type Output = Void
|
||||||
|
|
||||||
|
type State i =
|
||||||
|
{ elRef :: H.RefLabel
|
||||||
|
, inputval :: Input i
|
||||||
|
}
|
||||||
|
|
||||||
|
component :: H.Component HH.HTML (Query String) (Input String) Output Aff
|
||||||
|
component = mkComponent RawHTML
|
||||||
|
|
||||||
|
mkComponent :: forall i. (Input i -> RawHTML) -> H.Component HH.HTML (Query i) (Input i) Output Aff
|
||||||
|
mkComponent toRawHTML = H.lifecycleComponent
|
||||||
|
{ initialState: \inputval -> { elRef: H.RefLabel "inputval", inputval }
|
||||||
|
, render
|
||||||
|
, eval
|
||||||
|
, receiver: HE.input Receive
|
||||||
|
, initializer: Just $ H.action SetInnerHTML
|
||||||
|
, finalizer: Nothing
|
||||||
|
}
|
||||||
|
where
|
||||||
|
render :: (State i) -> H.ComponentHTML (Query i)
|
||||||
|
render state =
|
||||||
|
HH.div
|
||||||
|
[ HP.ref state.elRef ]
|
||||||
|
[]
|
||||||
|
|
||||||
|
eval :: (Query i) ~> H.ComponentDSL (State i) (Query i) Output Aff
|
||||||
|
eval = case _ of
|
||||||
|
SetInnerHTML a -> do
|
||||||
|
{ elRef } <- H.get
|
||||||
|
mel <- H.getHTMLElementRef elRef
|
||||||
|
for_ mel \el -> do
|
||||||
|
{ inputval } <- H.get
|
||||||
|
H.liftEffect (unsafeSetInnerHTML el (toRawHTML inputval))
|
||||||
|
pure a
|
||||||
|
|
||||||
|
Receive inputval a -> do
|
||||||
|
H.modify_ _ { inputval = inputval }
|
||||||
|
eval $ SetInnerHTML a
|
65
purs/src/Globals.js
Normal file
65
purs/src/Globals.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
exports._app = function() {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._closest = function(just, nothing, selector, el) {
|
||||||
|
var node = el.closest(selector);
|
||||||
|
if(node) {
|
||||||
|
return just(node);
|
||||||
|
} else {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._innerHtml = function(el) {
|
||||||
|
return el.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._setInnerHtml = function(content, el) {
|
||||||
|
el.innerHTML = content;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._createFormData = function(formElement) {
|
||||||
|
return new FormData(formElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._createFormString = function(formElement) {
|
||||||
|
return new URLSearchParams(new FormData(formElement)).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._createFormArray = function(formElement) {
|
||||||
|
return Array.from(new FormData(formElement));
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._getDataAttribute = function(name, el) {
|
||||||
|
return el.dataset[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._setDataAttribute = function(name, value, el) {
|
||||||
|
return el.dataset[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._moment8601 = function(tuple, s) {
|
||||||
|
var m = moment(s, moment.ISO_8601);
|
||||||
|
var s1 = m.fromNow();
|
||||||
|
var s2 = m.format('MMMM D YYYY, h:mm a') + " (" + m.format() + ") ";
|
||||||
|
return tuple(s1)(s2);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._mmoment8601 = function(just, nothing, tuple, s) {
|
||||||
|
try {
|
||||||
|
var m = moment(s, moment.ISO_8601);
|
||||||
|
var s1 = m.fromNow();
|
||||||
|
var s2 = m.format('MMMM D YYYY, h:mm a') + " (" + m.format() + ") ";
|
||||||
|
return just(tuple(s1)(s2));
|
||||||
|
} catch (error) {
|
||||||
|
return nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._closeWindow = function (window) {
|
||||||
|
window.close();
|
||||||
|
};
|
97
purs/src/Globals.purs
Normal file
97
purs/src/Globals.purs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
module Globals where
|
||||||
|
|
||||||
|
import Data.Function.Uncurried
|
||||||
|
|
||||||
|
import Data.Maybe (Maybe(..))
|
||||||
|
import Data.Nullable (Nullable, toMaybe)
|
||||||
|
import Data.Tuple (Tuple(..))
|
||||||
|
import Effect (Effect)
|
||||||
|
import Model (Bookmark)
|
||||||
|
import Prelude (Unit, pure, ($))
|
||||||
|
import Web.DOM (Element, Node)
|
||||||
|
import Web.HTML (HTMLElement, HTMLFormElement, Window)
|
||||||
|
import Web.XHR.FormData (FormData)
|
||||||
|
import Data.Newtype (class Newtype)
|
||||||
|
|
||||||
|
type App =
|
||||||
|
{ csrfHeaderName :: String
|
||||||
|
, csrfCookieName :: String
|
||||||
|
, csrfParamName :: String
|
||||||
|
, csrfToken :: String
|
||||||
|
, homeR :: String
|
||||||
|
, authRlogoutR :: String
|
||||||
|
, userR :: Nullable String
|
||||||
|
, noteR :: Nullable String
|
||||||
|
, dat :: AppData
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppData =
|
||||||
|
{ bmarks :: Array Bookmark
|
||||||
|
, bmark :: Bookmark
|
||||||
|
, isowner :: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
foreign import _app :: Fn0 App
|
||||||
|
|
||||||
|
app' :: Unit -> App
|
||||||
|
app' _ = runFn0 _app
|
||||||
|
|
||||||
|
foreign import _closest :: forall a. Fn4 (a -> Maybe a) (Maybe a) String Node (Maybe Node)
|
||||||
|
|
||||||
|
closest :: String -> Node -> Effect (Maybe Node)
|
||||||
|
closest selector node = pure $ runFn4 _closest Just Nothing selector node
|
||||||
|
|
||||||
|
foreign import _moment8601 :: Fn2 (String -> String -> Tuple String String) String (Tuple String String)
|
||||||
|
|
||||||
|
moment8601 :: String -> Effect (Tuple String String)
|
||||||
|
moment8601 s = pure $ runFn2 _moment8601 Tuple s
|
||||||
|
|
||||||
|
foreign import _mmoment8601 :: forall a. Fn4 (a -> Maybe a) (Maybe a) (String -> String -> Tuple String String) String (Maybe (Tuple String String))
|
||||||
|
|
||||||
|
mmoment8601 :: String -> Maybe (Tuple String String)
|
||||||
|
mmoment8601 s = runFn4 _mmoment8601 Just Nothing Tuple s
|
||||||
|
|
||||||
|
foreign import _innerHtml :: Fn1 HTMLElement String
|
||||||
|
|
||||||
|
innerHtml :: HTMLElement -> Effect String
|
||||||
|
innerHtml n = pure $ runFn1 _innerHtml n
|
||||||
|
|
||||||
|
foreign import _setInnerHtml :: Fn2 String HTMLElement HTMLElement
|
||||||
|
|
||||||
|
setInnerHtml :: String -> HTMLElement -> Effect HTMLElement
|
||||||
|
setInnerHtml c n = pure $ runFn2 _setInnerHtml c n
|
||||||
|
|
||||||
|
foreign import _createFormData :: Fn1 HTMLFormElement FormData
|
||||||
|
|
||||||
|
createFormData :: HTMLFormElement -> FormData
|
||||||
|
createFormData f = runFn1 _createFormData f
|
||||||
|
|
||||||
|
foreign import _createFormString :: Fn1 HTMLFormElement String
|
||||||
|
|
||||||
|
createFormString :: HTMLFormElement -> String
|
||||||
|
createFormString f = runFn1 _createFormString f
|
||||||
|
|
||||||
|
|
||||||
|
foreign import _createFormArray :: Fn1 HTMLFormElement (Array (Array String))
|
||||||
|
|
||||||
|
createFormArray :: HTMLFormElement -> (Array (Array String))
|
||||||
|
createFormArray f = runFn1 _createFormArray f
|
||||||
|
|
||||||
|
foreign import _getDataAttribute :: Fn2 String Element (Nullable String)
|
||||||
|
|
||||||
|
getDataAttribute :: String -> Element -> Effect (Maybe String)
|
||||||
|
getDataAttribute k n = pure $ toMaybe $ runFn2 _getDataAttribute k n
|
||||||
|
|
||||||
|
foreign import _setDataAttribute :: Fn3 String String Element Unit
|
||||||
|
|
||||||
|
setDataAttribute :: String -> String -> Element -> Effect Unit
|
||||||
|
setDataAttribute k v n = pure $ runFn3 _setDataAttribute k v n
|
||||||
|
|
||||||
|
foreign import _closeWindow :: Fn1 Window Unit
|
||||||
|
|
||||||
|
closeWindow :: Window -> Effect Unit
|
||||||
|
closeWindow win = pure $ runFn1 _closeWindow win
|
||||||
|
|
||||||
|
newtype RawHTML = RawHTML String
|
||||||
|
|
||||||
|
derive instance newtypeRawHTML :: Newtype RawHTML _
|
63
purs/src/Main.purs
Normal file
63
purs/src/Main.purs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import App (logout)
|
||||||
|
import Component.Add (addbmark)
|
||||||
|
import Component.BList (blist)
|
||||||
|
import Component.NList (nlist)
|
||||||
|
import Component.NNote (nnote)
|
||||||
|
import Component.AccountSettings (usetting)
|
||||||
|
import Data.Foldable (traverse_)
|
||||||
|
import Effect (Effect)
|
||||||
|
import Effect.Aff (Aff, launchAff)
|
||||||
|
import Effect.Class (liftEffect)
|
||||||
|
import Halogen.Aff as HA
|
||||||
|
import Halogen.VDom.Driver (runUI)
|
||||||
|
import Model (Bookmark, Note, AccountSettings)
|
||||||
|
import Web.DOM.Element (removeAttribute)
|
||||||
|
import Web.DOM.ParentNode (QuerySelector(..))
|
||||||
|
import Web.Event.Event (Event, preventDefault)
|
||||||
|
import Web.HTML.HTMLElement (toElement)
|
||||||
|
|
||||||
|
main :: Effect Unit
|
||||||
|
main = pure unit
|
||||||
|
|
||||||
|
logoutE :: Event -> Effect Unit
|
||||||
|
logoutE e = void <<< launchAff <<< logout =<< preventDefault e
|
||||||
|
|
||||||
|
renderBookmarks :: String -> Array Bookmark -> Effect Unit
|
||||||
|
renderBookmarks renderElSelector bmarks = do
|
||||||
|
HA.runHalogenAff do
|
||||||
|
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||||
|
void $ runUI (blist bmarks) unit el
|
||||||
|
showFooter
|
||||||
|
|
||||||
|
renderAddForm :: String -> Bookmark -> Effect Unit
|
||||||
|
renderAddForm renderElSelector bmark = do
|
||||||
|
HA.runHalogenAff do
|
||||||
|
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||||
|
runUI (addbmark bmark) unit el
|
||||||
|
|
||||||
|
renderNotes :: String -> Array Note -> Effect Unit
|
||||||
|
renderNotes renderElSelector notes = do
|
||||||
|
HA.runHalogenAff do
|
||||||
|
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||||
|
void $ runUI (nlist notes) unit el
|
||||||
|
showFooter
|
||||||
|
|
||||||
|
renderNote :: String -> Note -> Effect Unit
|
||||||
|
renderNote renderElSelector note = do
|
||||||
|
HA.runHalogenAff do
|
||||||
|
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||||
|
void $ runUI (nnote note) unit el
|
||||||
|
|
||||||
|
renderAccountSettings :: String -> AccountSettings -> Effect Unit
|
||||||
|
renderAccountSettings renderElSelector accountSettings = do
|
||||||
|
HA.runHalogenAff do
|
||||||
|
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||||
|
void $ runUI (usetting accountSettings) unit el
|
||||||
|
|
||||||
|
showFooter :: Aff Unit
|
||||||
|
showFooter = HA.selectElement (QuerySelector ".user_footer") >>= traverse_ \el ->
|
||||||
|
liftEffect $ removeAttribute "hidden" (toElement el)
|
7
purs/src/Marked.js
Normal file
7
purs/src/Marked.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
exports.markedImpl = function(str) {
|
||||||
|
marked.setOptions({
|
||||||
|
pedantic: false,
|
||||||
|
gfm: true
|
||||||
|
});
|
||||||
|
return marked(str);
|
||||||
|
};
|
9
purs/src/Marked.purs
Normal file
9
purs/src/Marked.purs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module Foreign.Marked where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
import Globals (RawHTML(..))
|
||||||
|
|
||||||
|
foreign import markedImpl :: String -> String
|
||||||
|
|
||||||
|
marked :: String -> RawHTML
|
||||||
|
marked = RawHTML <<< markedImpl
|
53
purs/src/Model.purs
Normal file
53
purs/src/Model.purs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
module Model where
|
||||||
|
|
||||||
|
import Data.Nullable (Nullable)
|
||||||
|
import Simple.JSON as J
|
||||||
|
|
||||||
|
type BookmarkId = Int
|
||||||
|
type TagId = Int
|
||||||
|
|
||||||
|
type Bookmark =
|
||||||
|
{ url :: String
|
||||||
|
, title :: String
|
||||||
|
, description :: String
|
||||||
|
, tags :: String
|
||||||
|
, private :: Boolean
|
||||||
|
, toread :: Boolean
|
||||||
|
, bid :: BookmarkId
|
||||||
|
, slug :: String
|
||||||
|
, selected :: Boolean
|
||||||
|
, time :: String
|
||||||
|
, archiveUrl :: Nullable String
|
||||||
|
}
|
||||||
|
|
||||||
|
newtype Bookmark' = Bookmark' Bookmark
|
||||||
|
derive newtype instance bookmark_rfI :: J.ReadForeign Bookmark'
|
||||||
|
derive newtype instance bookmark_wfI :: J.WriteForeign Bookmark'
|
||||||
|
|
||||||
|
type NoteId = Int
|
||||||
|
type NoteSlug = String
|
||||||
|
|
||||||
|
type Note =
|
||||||
|
{ id :: NoteId
|
||||||
|
, slug :: NoteSlug
|
||||||
|
, title :: String
|
||||||
|
, text :: String
|
||||||
|
, length :: Int
|
||||||
|
, isMarkdown :: Boolean
|
||||||
|
, created :: String
|
||||||
|
, updated :: String
|
||||||
|
}
|
||||||
|
|
||||||
|
newtype Note' = Note' Note
|
||||||
|
derive newtype instance note_rfI :: J.ReadForeign Note'
|
||||||
|
derive newtype instance note_wfI :: J.WriteForeign Note'
|
||||||
|
|
||||||
|
type AccountSettings =
|
||||||
|
{ archiveDefault :: Boolean
|
||||||
|
, privateDefault :: Boolean
|
||||||
|
, privacyLock :: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
newtype AccountSettings' = AccountSettings' AccountSettings
|
||||||
|
derive newtype instance usersettings_rfI :: J.ReadForeign AccountSettings'
|
||||||
|
derive newtype instance usersettings_wfI :: J.WriteForeign AccountSettings'
|
136
purs/src/Util.purs
Normal file
136
purs/src/Util.purs
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
module Util where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Control.Monad.Maybe.Trans (MaybeT(..))
|
||||||
|
import Data.Array (filter, find, mapMaybe)
|
||||||
|
import Data.Foldable (for_)
|
||||||
|
import Data.Maybe (Maybe(..), fromJust, fromMaybe, maybe)
|
||||||
|
import Data.Nullable (Nullable, toMaybe)
|
||||||
|
import Data.String (Pattern(..), Replacement(..), drop, replaceAll, split, take)
|
||||||
|
import Data.Tuple (Tuple(..), fst, snd)
|
||||||
|
import Effect (Effect)
|
||||||
|
import Global.Unsafe (unsafeDecodeURIComponent)
|
||||||
|
import Halogen (ClassName(..))
|
||||||
|
import Halogen.HTML as HH
|
||||||
|
import Halogen.HTML.Properties as HP
|
||||||
|
import Partial.Unsafe (unsafePartial)
|
||||||
|
import Web.DOM (Element, Node)
|
||||||
|
import Web.DOM.Document (toNonElementParentNode)
|
||||||
|
import Web.DOM.Element (fromNode, toParentNode)
|
||||||
|
import Web.DOM.NodeList (toArray)
|
||||||
|
import Web.DOM.NonElementParentNode (getElementById)
|
||||||
|
import Web.DOM.ParentNode (QuerySelector(..), querySelector, querySelectorAll)
|
||||||
|
import Web.HTML (HTMLDocument, Location, window)
|
||||||
|
import Web.HTML.HTMLDocument (body) as HD
|
||||||
|
import Web.HTML.HTMLDocument (toDocument)
|
||||||
|
import Web.HTML.HTMLElement (HTMLElement)
|
||||||
|
import Web.HTML.HTMLElement (fromElement) as HE
|
||||||
|
import Web.HTML.Location (search)
|
||||||
|
import Web.HTML.Window (document, location)
|
||||||
|
|
||||||
|
-- Halogen
|
||||||
|
|
||||||
|
class_ :: forall r i. String -> HP.IProp ( "class" :: String | r) i
|
||||||
|
class_ = HP.class_ <<< HH.ClassName
|
||||||
|
|
||||||
|
attr :: forall r i. String -> String -> HP.IProp r i
|
||||||
|
attr a = HP.attr (HH.AttrName a)
|
||||||
|
|
||||||
|
-- Util
|
||||||
|
|
||||||
|
_queryBoth :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Element -> Effect a) -> Effect Unit
|
||||||
|
_queryBoth (Tuple qa ea) (Tuple qb eb) f = do
|
||||||
|
ma <- _querySelector qa ea
|
||||||
|
mb <- _querySelector qb eb
|
||||||
|
for_ ma \a ->
|
||||||
|
for_ mb \b ->
|
||||||
|
f a b
|
||||||
|
|
||||||
|
_queryBoth' :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Array Node -> Effect a) -> Effect Unit
|
||||||
|
_queryBoth' (Tuple qa ea) (Tuple qb eb) f = do
|
||||||
|
ma <- _querySelector qa ea
|
||||||
|
bs <- _querySelectorAll qb eb
|
||||||
|
for_ ma \a ->
|
||||||
|
f a bs
|
||||||
|
|
||||||
|
_queryBoth'' :: forall a. Tuple String Element -> Tuple String Element -> (Array Node -> Array Node -> Effect a) -> Effect a
|
||||||
|
_queryBoth'' (Tuple qa ea) (Tuple qb eb) f = do
|
||||||
|
as <- _querySelectorAll qa ea
|
||||||
|
bs <- _querySelectorAll qb eb
|
||||||
|
f as bs
|
||||||
|
|
||||||
|
_querySelector :: String -> Element -> Effect (Maybe Element)
|
||||||
|
_querySelector s n = querySelector (QuerySelector s) (toParentNode n)
|
||||||
|
|
||||||
|
_querySelectorAll :: String -> Element -> Effect (Array Node)
|
||||||
|
_querySelectorAll s n = toArray =<< querySelectorAll (QuerySelector s) (toParentNode n)
|
||||||
|
|
||||||
|
_fromNode :: Node -> Element
|
||||||
|
_fromNode e = unsafePartial $ fromJust (fromNode e)
|
||||||
|
|
||||||
|
_fromElement :: Element -> HTMLElement
|
||||||
|
_fromElement e = unsafePartial $ fromJust (HE.fromElement e)
|
||||||
|
|
||||||
|
_getElementById :: String -> HTMLDocument -> Effect (Maybe Element)
|
||||||
|
_getElementById s = getElementById s <<< toNonElementParentNode <<< toDocument
|
||||||
|
|
||||||
|
_doc :: Effect HTMLDocument
|
||||||
|
_doc = document =<< window
|
||||||
|
|
||||||
|
_loc :: Effect Location
|
||||||
|
_loc = location =<< window
|
||||||
|
|
||||||
|
type QueryStringArray = Array (Tuple String (Maybe String))
|
||||||
|
|
||||||
|
_curQuerystring :: Effect QueryStringArray
|
||||||
|
_curQuerystring = do
|
||||||
|
loc <- _loc
|
||||||
|
srh <- search loc
|
||||||
|
pure $ _parseQueryString srh
|
||||||
|
|
||||||
|
_parseQueryString :: String -> QueryStringArray
|
||||||
|
_parseQueryString srh = do
|
||||||
|
let qs = let srh' = take 1 srh in if (srh' == "#" || srh' == "?") then drop 1 srh else srh
|
||||||
|
mapMaybe go $ (filter (_ /= "") <<< split (Pattern "&")) qs
|
||||||
|
where
|
||||||
|
decode = unsafeDecodeURIComponent <<< replaceAll (Pattern "+") (Replacement " ")
|
||||||
|
go kv =
|
||||||
|
case split (Pattern "=") kv of
|
||||||
|
[k] -> Just (Tuple (decode k) Nothing)
|
||||||
|
[k, v] -> Just (Tuple (decode k) (Just (decode v)))
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
_lookupQueryStringValue :: QueryStringArray -> String -> Maybe String
|
||||||
|
_lookupQueryStringValue qs k = do
|
||||||
|
join $ map snd $ find ((_ == k) <<< fst) qs
|
||||||
|
|
||||||
|
_body :: Effect HTMLElement
|
||||||
|
_body = unsafePartial $ pure <<< fromJust =<< HD.body =<< _doc
|
||||||
|
|
||||||
|
_mt :: forall a. Effect (Maybe a) -> MaybeT Effect a
|
||||||
|
_mt = MaybeT
|
||||||
|
|
||||||
|
_mt_pure :: forall a. Maybe a -> MaybeT Effect a
|
||||||
|
_mt_pure = MaybeT <<< pure
|
||||||
|
|
||||||
|
dummyAttr :: forall r i. HP.IProp r i
|
||||||
|
dummyAttr = HP.attr (HH.AttrName "data-dummy") ""
|
||||||
|
|
||||||
|
whenP :: forall r i. Boolean -> HP.IProp r i -> HP.IProp r i
|
||||||
|
whenP b p = if b then p else dummyAttr
|
||||||
|
|
||||||
|
maybeP :: forall a r i. Maybe a -> (a -> HP.IProp r i) -> HP.IProp r i
|
||||||
|
maybeP m p = maybe dummyAttr p m
|
||||||
|
|
||||||
|
whenC :: Boolean -> ClassName -> ClassName
|
||||||
|
whenC b c = if b then c else ClassName ""
|
||||||
|
|
||||||
|
whenH :: forall p i. Boolean -> (Unit -> HH.HTML p i) -> HH.HTML p i
|
||||||
|
whenH b k = if b then k unit else HH.text ""
|
||||||
|
|
||||||
|
maybeH :: forall a p i. Maybe a -> (a -> HH.HTML p i) -> HH.HTML p i
|
||||||
|
maybeH m k = maybe (HH.text "") k m
|
||||||
|
|
||||||
|
fromNullableStr :: Nullable String -> String
|
||||||
|
fromNullableStr = fromMaybe "" <<< toMaybe
|
9
purs/test/Main.purs
Normal file
9
purs/test/Main.purs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module Test.Main where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
import Effect (Effect)
|
||||||
|
import Effect.Console (log)
|
||||||
|
|
||||||
|
main :: Effect Unit
|
||||||
|
main = do
|
||||||
|
log "You should add some tests."
|
88
sample-bookmarks.json
Normal file
88
sample-bookmarks.json
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
[{"href":"http://math.andrej.com/2012/11/08/how-to-implement-dependent-type-theory-i/","description":"How to implement dependent type theory I | Mathematics and Computation","extended":"","time":"2018-03-02T21:37:18Z","shared":"yes","toread":"no","tags":"dependenttypes"},
|
||||||
|
{"href":"https://www.newyorker.com/magazine/2015/05/11/overkill-atul-gawande","description":"America\u2019s Epidemic of Unnecessary Care | The New Yorker","extended":"","time":"2018-03-02T19:26:55Z","shared":"yes","toread":"no","tags":"medicie health"},
|
||||||
|
{"href":"https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis","description":"Functional options for friendly APIs | Dave Cheney","extended":"","time":"2018-03-02T19:24:40Z","shared":"yes","toread":"no","tags":"functionaloptions golang"},
|
||||||
|
{"href":"https://github.com/swagger-api/swagger-codegen/wiki/Swagger-Codegen-migration-(swagger-codegen-generators-repository)","description":"Swagger Codegen migration (swagger codegen generators repository) \u00b7 swagger-api/swagger-codegen Wiki","extended":"","time":"2018-03-02T16:06:09Z","shared":"yes","toread":"no","tags":"swagger-codegen"},
|
||||||
|
{"href":"https://news.ycombinator.com/item?id=16493727","description":"LimeSDR Now Backed by the European Space Agency | Hacker News","extended":"","time":"2018-03-02T15:52:26Z","shared":"yes","toread":"no","tags":"sdr"},
|
||||||
|
{"href":"http://www.tobiastoft.com/posts/an-intro-to-pen-plotters","description":"An intro to Pen Plotters \u2014 Hej.","extended":"","time":"2018-03-02T02:50:35Z","shared":"yes","toread":"no","tags":"penplotter"},
|
||||||
|
{"href":"https://news.ycombinator.com/item?id=16493489","description":"Machine Learning Crash Course | Hacker News","extended":"","time":"2018-03-01T19:53:53Z","shared":"yes","toread":"no","tags":"machinelearning"},
|
||||||
|
{"href":"https://developers.google.com/machine-learning/crash-course/","description":"Machine Learning Crash Course \u00a0|\u00a0 Google Developers","extended":"","time":"2018-03-01T19:23:58Z","shared":"yes","toread":"no","tags":"machinelearning"},
|
||||||
|
{"href":"https://www.reaktor.com/blog/fear-trust-and-javascript/","description":"Fear, trust and JavaScript: When types and functional programming fail - Reaktor","extended":"","time":"2018-03-01T19:05:12Z","shared":"yes","toread":"no","tags":""},
|
||||||
|
{"href":"http://jxv.io/blog/2018-02-28-A-Game-in-Haskell.html","description":"A Game in Haskell - Dino Rush","extended":"","time":"2018-03-01T13:33:10Z","shared":"yes","toread":"no","tags":"game"},
|
||||||
|
{"href":"https://arstechnica.com/science/2018/02/signal-of-the-universes-first-stars-hints-at-odd-form-of-dark-matter/","description":"Simple telescope picks up hint of the Universe\u2019s first stars, dark matter | Ars Technica","extended":"","time":"2018-03-01T07:19:56Z","shared":"yes","toread":"no","tags":"darkmatter"},
|
||||||
|
{"href":"https://blogs.scientificamerican.com/observations/a-potentially-game-changing-message-from-the-dawn-of-time/","description":"A Potentially Game-Changing Message from the Dawn of Time - Scientific American Blog Network","extended":"","time":"2018-03-01T07:19:51Z","shared":"yes","toread":"no","tags":"darkmatter"},
|
||||||
|
{"href":"https://www.nature.com/articles/d41586-018-02616-8","description":"Astronomers detect light from the Universe\u2019s first stars","extended":"","time":"2018-03-01T07:19:48Z","shared":"yes","toread":"no","tags":"darkmatter"},
|
||||||
|
{"href":"https://www.nature.com/articles/nature25791","description":"Possible interaction between baryons and dark-matter particles revealed by the first stars | Nature","extended":"","time":"2018-03-01T07:19:44Z","shared":"yes","toread":"no","tags":"darkmatter"},
|
||||||
|
{"href":"https://www.theverge.com/2018/2/27/17054740/palantir-predictive-policing-tool-new-orleans-nopd","description":"Palantir has secretly been using New Orleans to test its predictive policing technology - The Verge","extended":"","time":"2018-03-01T03:40:47Z","shared":"yes","toread":"no","tags":"police"},
|
||||||
|
{"href":"https://aaronweiss.us/posts/2018-02-26-reasoning-with-types-in-rust.html","description":"Aaron Weiss / Reasoning with Types in Rust","extended":"","time":"2018-03-01T01:46:51Z","shared":"yes","toread":"no","tags":"rust"},
|
||||||
|
{"href":"https://www.washingtonpost.com/opinions/hope-hicks-says-she-lies-for-trump-thats-encouraging/2018/02/28/09e61982-1cc3-11e8-9de1-147dd2df3829_story.html","description":"Hope Hicks told the truth about lying for Trump. Now she\u2019s gone. - The Washington Post","extended":"","time":"2018-03-01T01:42:24Z","shared":"yes","toread":"no","tags":"hopehicks"},
|
||||||
|
{"href":"https://www.0x0ff.info/wp-content/uploads/2014/02/cheat-sheet.png","description":"cheat-sheet.png (3508\u00d72479)","extended":"","time":"2018-02-28T22:06:47Z","shared":"yes","toread":"no","tags":""},
|
||||||
|
{"href":"http://socialturkers.com/","description":"social turkers","extended":"socialturkers","time":"2018-02-28T21:33:27Z","shared":"yes","toread":"no","tags":"socialturkers"},
|
||||||
|
{"href":"http://networkcultures.org/moneylab/2018/02/07/the-blockchain-as-a-modulator-of-existence/","description":"MoneyLab | The Blockchain as a Modulator of Existence","extended":"","time":"2018-02-28T21:20:52Z","shared":"yes","toread":"no","tags":"blockchain"},
|
||||||
|
{"href":"http://neilmitchell.blogspot.com/2008/02/adding-data-files-using-cabal.html","description":"Neil Mitchell's Haskell Blog: Adding data files using Cabal","extended":"","time":"2018-02-28T21:14:53Z","shared":"yes","toread":"no","tags":"haskell"},
|
||||||
|
{"href":"https://github.com/jkachmar/jkachmar-lambdaconf-cfp-2018/blob/master/commented-cfp.md","description":"jkachmar-lambdaconf-cfp-2018/commented-cfp.md at master \u00b7 jkachmar/jkachmar-lambdaconf-cfp-2018","extended":"","time":"2018-02-28T20:22:04Z","shared":"yes","toread":"no","tags":"lambdaconf"},
|
||||||
|
{"href":"https://gist.github.com/jkachmar/f1e1544524820ad6eb49524a2327d3a8","description":"Servant EKG 0.12","extended":"","time":"2018-02-28T19:58:46Z","shared":"yes","toread":"no","tags":"jkachmar"},
|
||||||
|
{"href":"https://gist.github.com/jkachmar/b3baedc4f3eacce3d6cc8cb790447eb7","description":"Esqueleto tests for SqlReadT","extended":"","time":"2018-02-28T19:58:29Z","shared":"yes","toread":"no","tags":"jkachmar"},
|
||||||
|
{"href":"https://stackoverflow.com/questions/48954495/is-it-possible-to-get-all-contexts-of-a-traversable-lazily","description":"haskell - Is it possible to get all contexts of a Traversable lazily? - Stack Overflow","extended":"","time":"2018-02-28T19:50:16Z","shared":"yes","toread":"no","tags":"haskell"},
|
||||||
|
{"href":"https://blog.github.com/2017-11-29-use-any-theme-with-github-pages/","description":"Use any theme with GitHub Pages | The GitHub Blog","extended":"","time":"2018-02-28T19:46:33Z","shared":"yes","toread":"no","tags":"theme"},
|
||||||
|
{"href":"https://mtlynch.io/why-i-quit-google/","description":"Why I Quit Google to Work for Myself - Silly Bits","extended":"","time":"2018-02-28T19:44:38Z","shared":"yes","toread":"no","tags":"google"},
|
||||||
|
{"href":"http://www.olioapps.com/blog/the-lost-art-of-the-makefile/","description":"The Lost Art of the Makefile","extended":"","time":"2018-02-28T19:22:19Z","shared":"yes","toread":"no","tags":"makefile"},
|
||||||
|
{"href":"https://www.youtube.com/results?search_query=Politicon","description":"Politicon - YouTube","extended":"","time":"2018-02-28T17:04:12Z","shared":"yes","toread":"no","tags":"Politicon"},
|
||||||
|
{"href":"https://politicon.com/about-politicon/","description":"\u00bb About Politicon","extended":"","time":"2018-02-28T17:03:48Z","shared":"yes","toread":"no","tags":"Politicon"},
|
||||||
|
{"href":"https://www.facebook.com/TheYoungTurks/videos/10155481460764205/","description":"The Young Turks - Ben Shapiro, the self proclaimed free speech...","extended":"","time":"2018-02-28T17:00:37Z","shared":"yes","toread":"no","tags":"tyt"},
|
||||||
|
{"href":"https://please.build/","description":"Please","extended":"","time":"2018-02-28T16:46:30Z","shared":"yes","toread":"no","tags":"please build"},
|
||||||
|
{"href":"https://camdavidsonpilon.github.io/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers/","description":"Bayesian Methods for Hackers","extended":"","time":"2018-02-28T16:38:26Z","shared":"yes","toread":"no","tags":"bayesian probability python statistics"},
|
||||||
|
{"href":"https://www.tweag.io/posts/2018-02-28-bazel-haskell.html","description":"Tweag I/O - Build large polyglot projects with Bazel... now with Haskell support","extended":"","time":"2018-02-28T15:13:00Z","shared":"yes","toread":"no","tags":""},
|
||||||
|
{"href":"http://www.parsonsmatt.org/2016/12/18/servant_in_yesod_-_yo_dawg.html","description":"Servant in Yesod - Yo Dawg","extended":"","time":"2018-02-27T21:21:53Z","shared":"yes","toread":"no","tags":"yesod"},
|
||||||
|
{"href":"https://ghc.haskell.org/trac/ghc/ticket/9706","description":"#9706 (New block-structured heap organization for 64-bit) \u2013 GHC","extended":"","time":"2018-02-27T17:09:26Z","shared":"yes","toread":"no","tags":"ghc"},
|
||||||
|
{"href":"https://github.com/Microsoft/WSL/issues/1671","description":"`stack ghc` painfully slow \u00b7 Issue #1671 \u00b7 Microsoft/WSL","extended":"","time":"2018-02-27T17:07:48Z","shared":"yes","toread":"no","tags":"ghc wsl"},
|
||||||
|
{"href":"https://www.atlasobscura.com/articles/blankets-summer-hot","description":"Why Do We Sleep Under Blankets, Even on the Hottest Nights? - Atlas Obscura","extended":"","time":"2018-02-27T16:43:23Z","shared":"yes","toread":"no","tags":"blankets"},
|
||||||
|
{"href":"http://www.leshatton.org/Documents/OO_IS698.pdf","description":"","extended":"","time":"2018-02-27T16:31:02Z","shared":"yes","toread":"no","tags":"oo OO_IS698"},
|
||||||
|
{"href":"http://gameprogrammingpatterns.com/command.html","description":"Command \u00b7 Design Patterns Revisited \u00b7 Game Programming Patterns","extended":"","time":"2018-02-27T16:17:35Z","shared":"yes","toread":"yes","tags":"command"},
|
||||||
|
{"href":"https://ericlippert.com/2015/05/04/wizards-and-warriors-part-three/","description":"Wizards and warriors, part three | Fabulous adventures in coding","extended":"","time":"2018-02-26T22:58:37Z","shared":"yes","toread":"no","tags":"ericlippert"},
|
||||||
|
{"href":"https://ericlippert.com/2015/05/07/wizards-and-warriors-part-four/","description":"Wizards and warriors, part four | Fabulous adventures in coding","extended":"","time":"2018-02-26T22:58:36Z","shared":"yes","toread":"no","tags":"ericlippert"},
|
||||||
|
{"href":"https://ericlippert.com/2015/05/11/wizards-and-warriors-part-five/","description":"Wizards and warriors, part five | Fabulous adventures in coding","extended":"","time":"2018-02-26T22:58:32Z","shared":"yes","toread":"no","tags":"ericlippert"},
|
||||||
|
{"href":"https://ericlippert.com/2015/04/30/wizards-and-warriors-part-two/","description":"Wizards and warriors, part two | Fabulous adventures in coding","extended":"","time":"2018-02-26T22:58:23Z","shared":"yes","toread":"no","tags":"ericlippert"},
|
||||||
|
{"href":"https://ericlippert.com/2015/04/27/wizards-and-warriors-part-one/","description":"Wizards and warriors, part one | Fabulous adventures in coding","extended":"","time":"2018-02-26T22:58:16Z","shared":"yes","toread":"no","tags":"ericlippert"},
|
||||||
|
{"href":"https://news.ycombinator.com/item?id=16468280","description":"How I Learned to Stop Worrying and Love the State Machine | Hacker News","extended":"When I'm stuck on a software design problem, pick some random part of the program and see what happens if I make it first class.","time":"2018-02-26T22:57:38Z","shared":"yes","toread":"no","tags":"raganwald"},
|
||||||
|
{"href":"http://raganwald.com/2018/02/23/forde.html","description":"Forde's Tenth Rule, or, \"How I Learned to Stop Worrying and \u2764\ufe0f the State Machine\"","extended":"","time":"2018-02-26T22:57:20Z","shared":"yes","toread":"yes","tags":"raganwald"},
|
||||||
|
{"href":"http://downloads.haskell.org/~ghc/latest/docs/html/users_guide/flags.html","description":"7.6. Flag reference \u2014 Glasgow Haskell Compiler 8.2.2 User's Guide","extended":"-fprint-expanded-synonyms","time":"2018-02-26T21:52:02Z","shared":"yes","toread":"no","tags":""},
|
||||||
|
{"href":"https://www.google.com/search?q=codetermination&oq=codetermination&aqs=chrome..69i57.1655j0j7&sourceid=chrome&ie=UTF-8","description":"codetermination - Google Search","extended":"","time":"2018-02-26T20:38:10Z","shared":"yes","toread":"no","tags":"codetermination"},
|
||||||
|
{"href":"https://news.ycombinator.com/item?id=16463069","description":"Why can\u2019t women get pregnant without the menstrual cycle? (2016) | Hacker News","extended":"","time":"2018-02-26T13:24:34Z","shared":"yes","toread":"no","tags":""},
|
||||||
|
{"href":"http://beej.us/guide/bgnet/html/multi/index.html","description":"Beej's Guide to Network Programming","extended":"","time":"2018-02-25T21:25:03Z","shared":"yes","toread":"no","tags":"beej network"},
|
||||||
|
{"href":"https://github.com/mqtt/mqtt.github.io/wiki/Basic-Concepts","description":"Basic Concepts \u00b7 mqtt/mqtt.github.io Wiki","extended":"","time":"2018-02-25T21:23:13Z","shared":"yes","toread":"no","tags":"mqtt"},
|
||||||
|
{"href":"http://mqtt.org/","description":"MQTT","extended":"","time":"2018-02-25T21:22:42Z","shared":"yes","toread":"no","tags":"mqtt"},
|
||||||
|
{"href":"https://www.thethingsnetwork.org/docs/network/cli/quick-start.html","description":"Quick Start | The Things Network","extended":"","time":"2018-02-25T21:20:22Z","shared":"yes","toread":"no","tags":"ttn"},
|
||||||
|
{"href":"https://www.thethingsnetwork.org/forum/t/chicago-illinois/351","description":"Chicago, Illinois - Communities / Find people from your city or area - The Things Network","extended":"","time":"2018-02-25T21:19:15Z","shared":"yes","toread":"no","tags":"chicago"},
|
||||||
|
{"href":"https://www.thethingsnetwork.org/community/chicago/","description":"Chicago - The Things Network Community","extended":"","time":"2018-02-25T21:14:24Z","shared":"yes","toread":"no","tags":"lora"},
|
||||||
|
{"href":"https://jozefg.bitbucket.io/posts/2015-01-08-modules.html","description":"C&C - A Crash Course on ML Modules","extended":"","time":"2018-02-25T19:50:12Z","shared":"yes","toread":"no","tags":"ml modules"},
|
||||||
|
{"href":"https://defn.io/2018/02/25/web-app-from-scratch-01/","description":"Web application from scratch, Part I \u00b7 Bogdan Popa","extended":"","time":"2018-02-25T19:41:47Z","shared":"yes","toread":"no","tags":"pythom"},
|
||||||
|
{"href":"https://news.ycombinator.com/item?id=16456792","description":"Overconfident Students, Dubious Employers | Hacker News","extended":"I'm always a little skeptical of these sorts of surveys because it's hard to tease out what people believe about themselves because it's true vs. what people believe about themselves because it's useful.\r\nI remember that when I was a new grad, there was a very large part of myself that held a realistic appraisal of my abilities and was therefore scared shitless about my ability to make it in the working world. I was very careful to never let that part of me out in interviews - or, for that matter, to anyone. Confidence only works if you keep up the illusion so thoroughly that it ceases to be an illusion.\r\n\r\nAnd it worked. I got a job at a financial software startup, and then was put in charge of projects that no new grad should ever have been put in charge of. I grew into the role. I left to go found a startup, which is also something that someone with 2 years of work experience had no business doing. That worked too - I may not have been qualified to found a startup, but when I folded it up, I was a lot more qualified as an engineer than most of my other peers with 4 years of work experience. So Google hired me to work on the front page of the search engine, and I grew into that role too.\r\n\r\nThe majority of my classmates let their accurate perceptions of what they were actually qualified to do govern what they applied to do, and as a result, many were still struggling to get into a career 10 years later. By that point, your self-perception has become reality, and it's much harder to convince potential employers to take the risk that you'll grow into the position. Then they wake up and realize that everybody's faking it and their new manager isn't actually all that much more skilled than them, but (barring a career reset like going to grad school) it's difficult to reset people's perceptions.","time":"2018-02-25T13:55:18Z","shared":"yes","toread":"no","tags":"hn"},
|
||||||
|
{"href":"https://comminos.com/css/default.css","description":"","extended":"","time":"2018-02-25T04:25:50Z","shared":"yes","toread":"no","tags":"css"},
|
||||||
|
{"href":"https://www.yesodweb.com/blog/2012/08/classy-prelude-good-bad-ugly","description":"ClassyPrelude: The good, the bad, and the ugly","extended":"","time":"2018-02-24T21:32:59Z","shared":"yes","toread":"no","tags":"classyprelude"},
|
||||||
|
{"href":"https://medium.com/incerto/the-most-intolerant-wins-the-dictatorship-of-the-small-minority-3f1f83ce4e15","description":"The Most Intolerant Wins: The Dictatorship of the Small Minority","extended":"","time":"2018-02-24T19:24:12Z","shared":"yes","toread":"no","tags":"taleb"},
|
||||||
|
{"href":"https://www.quora.com/Define-INFRINGED-as-it-is-used-in-the-second-amendment-What-is-unclear-about-this-If-lawmakers-want-to-infringe-upon-our-right-to-bear-arms-why-dont-they-follow-the-law-and-amend-the-Constitution","description":"Define 'INFRINGED' as it is used in the second amendment. What is unclear about this? If lawmakers want to infringe upon our right to bear arms, why don't they follow the law and amend the Constitution? - Quora","extended":"","time":"2018-02-24T18:09:32Z","shared":"yes","toread":"no","tags":"infringed"},
|
||||||
|
{"href":"https://mitpress.mit.edu/books/functional-differential-geometry","description":"Functional Differential Geometry | The MIT Press","extended":"","time":"2018-02-24T18:02:29Z","shared":"yes","toread":"no","tags":"physics"},
|
||||||
|
{"href":"https://mitpress.mit.edu/sites/default/files/titles/content/sicm_edition_2/book.html","description":"Structure and Interpretation of Classical Mechanics","extended":"","time":"2018-02-24T17:59:27Z","shared":"yes","toread":"no","tags":"sicp"},
|
||||||
|
{"href":"https://www.nytimes.com/2018/02/23/opinion/brian-mast-assault-weapons-ban.html?mtrref=www.facebook.com&gwh=B320C7668EBB9993E9F5B4BC712DB4D7&gwt=pay&assetType=opinion","description":"","extended":"","time":"2018-02-24T16:57:13Z","shared":"yes","toread":"no","tags":""},
|
||||||
|
{"href":"http://sqlite.org/lang_createtable.html#uniqueconst","description":"SQLite Query Language: CREATE TABLE","extended":"","time":"2018-02-24T04:26:51Z","shared":"yes","toread":"no","tags":"sqlite unique"},
|
||||||
|
{"href":"http://cockpit-project.org/","description":"Cockpit Project \u2014 Cockpit Project","extended":"","time":"2018-02-24T00:04:00Z","shared":"yes","toread":"no","tags":""},
|
||||||
|
{"href":"http://baatz.io/posts/haskell-in-a-startup/","description":"A founder's perspective on 4 years with Haskell","extended":"","time":"2018-02-23T20:38:27Z","shared":"yes","toread":"no","tags":"haskell"},
|
||||||
|
{"href":"https://github.com/trending/haskell","description":"Trending Haskell repositories on GitHub today","extended":"","time":"2018-02-23T20:35:00Z","shared":"yes","toread":"no","tags":"trending github haskell"},
|
||||||
|
{"href":"https://newrepublic.com/article/147111/zadie-smiths-book-essays-explores-means-human","description":"Zadie Smith\u2019s Book of Essays Explores What It Means to Be Human | New Republic","extended":"","time":"2018-02-23T20:33:55Z","shared":"yes","toread":"yes","tags":"zadiesmith lit"},
|
||||||
|
{"href":"https://nymag.com/daily/intelligencer/2018/02/americas-opioid-epidemic.html","description":"Andrew Sullivan on the Opioid Epidemic in America","extended":"","time":"2018-02-23T19:11:10Z","shared":"yes","toread":"no","tags":"editorial"},
|
||||||
|
{"href":"https://news.ycombinator.com/item?id=16445950","description":"The Poison We Pick | Hacker News","extended":"","time":"2018-02-23T19:11:02Z","shared":"yes","toread":"yes","tags":"editorial"},
|
||||||
|
{"href":"https://ftalphaville.ft.com/2018/02/15/2198809/someone-is-wrong-on-the-internet-millennial-savings-edition/","description":"Someone is wrong on the internet, millennial savings edition | FT Alphaville","extended":"","time":"2018-02-23T19:10:15Z","shared":"yes","toread":"no","tags":"housing"},
|
||||||
|
{"href":"https://speakerdeck.com/justinwoo/easy-json-deserialization-with-simple-json-and-record","description":"Easy JSON deserialization with Simple-JSON and Record // Speaker Deck","extended":"","time":"2018-02-23T07:19:52Z","shared":"yes","toread":"no","tags":"haskell"},
|
||||||
|
{"href":"http://www.ikea.com/us/en/catalog/products/00103102/","description":"MARKUS Swivel chair - Glose black - IKEA","extended":"","time":"2018-02-23T05:23:13Z","shared":"yes","toread":"no","tags":"Ikea Markus"},
|
||||||
|
{"href":"https://kono.store/products/kira-mechanical-keyboard","description":"Kira Mechanical Keyboard \u2013 Kono Store","extended":"","time":"2018-02-23T05:19:26Z","shared":"yes","toread":"no","tags":"kira"},
|
||||||
|
{"href":"https://www.reddit.com/r/MechanicalKeyboards/comments/7zhhje/battlestation/","description":"Battlestation : MechanicalKeyboards","extended":"","time":"2018-02-23T05:15:59Z","shared":"yes","toread":"no","tags":"keyboard"},
|
||||||
|
{"href":"https://soundcloud.com/jordanpetersonpodcast","description":"The Jordan B Peterson Podcast | Free Listening on SoundCloud","extended":"","time":"2018-02-23T05:13:37Z","shared":"yes","toread":"no","tags":"jordanpetersonpodcast"},
|
||||||
|
{"href":"https://www.theatlantic.com/politics/archive/2018/02/what-i-saw-treating-the-victims-from-parkland-should-change-the-debate-on-guns/553937/","description":"The AR-15 Is Different: What I Learned Treating Parkland Victims - The Atlantic","extended":"","time":"2018-02-23T05:00:55Z","shared":"yes","toread":"no","tags":"ar15"},
|
||||||
|
{"href":"https://jacobian.org/writing/python-environment-2018/","description":"My Python Development Environment, 2018 Edition \u00ab Jacob Kaplan-Moss","extended":"","time":"2018-02-22T22:50:41Z","shared":"yes","toread":"no","tags":"python"},
|
||||||
|
{"href":"https://www.youtube.com/watch?v=F4VZPxLZUdA&=&t=1625s","description":"Building test check Generators - Gary Fredericks - YouTube","extended":"","time":"2018-02-22T22:45:33Z","shared":"yes","toread":"no","tags":"propertybasedtesting"},
|
||||||
|
{"href":"https://keybase.io/docs/the_app/linux_expired_key","description":"Keybase","extended":"","time":"2018-02-22T22:23:28Z","shared":"yes","toread":"no","tags":"keybase"},
|
||||||
|
{"href":"https://underscore.io/blog/posts/2017/06/02/uniting-church-and-state.html","description":"Uniting Church and State: FP and OO Together - Underscore","extended":"","time":"2018-02-22T20:57:05Z","shared":"yes","toread":"no","tags":"fpoo"},
|
||||||
|
{"href":"https://www.cnn.com/2018/02/22/politics/cnn-town-hall-full-video-transcript/index.html","description":"CNN town hall: Students question lawmakers, NRA (full transcript, video) - CNNPolitics","extended":"","time":"2018-02-22T20:52:16Z","shared":"yes","toread":"no","tags":"cnn town hall sunrise"},
|
||||||
|
{"href":"http://yourbasic.org/golang/your-basic-func/","description":"Your basic func | yourbasic.org","extended":"","time":"2018-02-22T20:19:55Z","shared":"yes","toread":"no","tags":"graph golang"},
|
||||||
|
{"href":"https://operand.ca/2018/02/22/liberating-a-x200.html","description":"","extended":"","time":"2018-02-22T20:16:34Z","shared":"yes","toread":"no","tags":"thinkpad"},
|
||||||
|
{"href":"https://arxiv.org/abs/1802.07228","description":"[1802.07228] The Malicious Use of Artificial Intelligence: Forecasting, Prevention, and Mitigation","extended":"","time":"2018-02-22T20:10:25Z","shared":"yes","toread":"no","tags":"malevolent"}]
|
3
sample-migrate.sh
Executable file
3
sample-migrate.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
stack exec migration -- createdb --conn espial.sqlite3
|
||||||
|
stack exec migration -- createuser --conn espial.sqlite3 --userName myusername --userPassword myuserpassword
|
||||||
|
stack exec migration -- importbookmarks --conn espial.sqlite3 --userName myusername --bookmarkFile sample-bookmarks.json
|
182
src/Application.hs
Normal file
182
src/Application.hs
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Application
|
||||||
|
( getApplicationDev
|
||||||
|
, appMain
|
||||||
|
, develMain
|
||||||
|
, makeFoundation
|
||||||
|
, makeLogWare
|
||||||
|
-- * for DevelMain
|
||||||
|
, getApplicationRepl
|
||||||
|
, shutdownApp
|
||||||
|
-- * for GHCI
|
||||||
|
, handler
|
||||||
|
, db
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Monad.Logger (liftLoc, runLoggingT)
|
||||||
|
import Database.Persist.Sqlite
|
||||||
|
(createSqlitePool, sqlDatabase, sqlPoolSize)
|
||||||
|
import Import
|
||||||
|
import Yesod.Auth (getAuth)
|
||||||
|
import Language.Haskell.TH.Syntax (qLocation)
|
||||||
|
import Lens.Micro
|
||||||
|
import Network.HTTP.Client.TLS
|
||||||
|
import Network.Wai (Middleware)
|
||||||
|
import Network.Wai.Middleware.Autohead
|
||||||
|
import Network.Wai.Middleware.AcceptOverride
|
||||||
|
import Network.Wai.Middleware.Gzip
|
||||||
|
import Network.Wai.Middleware.MethodOverride
|
||||||
|
import Network.Wai.Handler.Warp
|
||||||
|
(Settings, defaultSettings, defaultShouldDisplayException,
|
||||||
|
runSettings, setHost, setOnException, setPort, getPort)
|
||||||
|
import Network.Wai.Middleware.RequestLogger
|
||||||
|
(Destination(Logger), IPAddrSource(..), OutputFormat(..),
|
||||||
|
destination, mkRequestLogger, outputFormat)
|
||||||
|
import System.Log.FastLogger
|
||||||
|
(defaultBufSize, newStdoutLoggerSet, toLogStr)
|
||||||
|
|
||||||
|
import qualified Control.Monad.Metrics as MM
|
||||||
|
import qualified Network.Wai.Metrics as WM
|
||||||
|
import qualified System.Metrics as EKG
|
||||||
|
import qualified System.Remote.Monitoring as EKG
|
||||||
|
|
||||||
|
-- Import all relevant handler modules here.
|
||||||
|
-- Don't forget to add new modules to your cabal file!
|
||||||
|
import Handler.Common
|
||||||
|
import Handler.Home
|
||||||
|
import Handler.User
|
||||||
|
import Handler.AccountSettings
|
||||||
|
import Handler.Add
|
||||||
|
import Handler.Edit
|
||||||
|
import Handler.Notes
|
||||||
|
import Handler.Docs
|
||||||
|
|
||||||
|
mkYesodDispatch "App" resourcesApp
|
||||||
|
|
||||||
|
makeFoundation :: AppSettings -> IO App
|
||||||
|
makeFoundation appSettings = do
|
||||||
|
appHttpManager <- getGlobalManager
|
||||||
|
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
|
||||||
|
store <- EKG.newStore
|
||||||
|
EKG.registerGcMetrics store
|
||||||
|
appMetrics <- MM.initializeWith store
|
||||||
|
appStatic <-
|
||||||
|
(if appMutableStatic appSettings
|
||||||
|
then staticDevel
|
||||||
|
else static)
|
||||||
|
(appStaticDir appSettings)
|
||||||
|
let mkFoundation appConnPool = App { ..}
|
||||||
|
tempFoundation = mkFoundation (error "connPool forced in tempFoundation")
|
||||||
|
logFunc = messageLoggerSource tempFoundation appLogger
|
||||||
|
pool <-
|
||||||
|
flip runLoggingT logFunc $
|
||||||
|
createSqlitePool
|
||||||
|
(sqlDatabase (appDatabaseConf appSettings))
|
||||||
|
(sqlPoolSize (appDatabaseConf appSettings))
|
||||||
|
-- runLoggingT
|
||||||
|
-- (runSqlPool runMigrations pool)
|
||||||
|
-- logFunc
|
||||||
|
return (mkFoundation pool)
|
||||||
|
|
||||||
|
makeApplication :: App -> IO Application
|
||||||
|
makeApplication foundation = do
|
||||||
|
logWare <- makeLogWare foundation
|
||||||
|
appPlain <- toWaiAppPlain foundation
|
||||||
|
let store = appMetrics foundation ^. MM.metricsStore
|
||||||
|
waiMetrics <- WM.registerWaiMetrics store
|
||||||
|
return (logWare (makeMiddleware waiMetrics appPlain))
|
||||||
|
|
||||||
|
makeMiddleware :: WM.WaiMetrics -> Middleware
|
||||||
|
makeMiddleware waiMetrics =
|
||||||
|
WM.metrics waiMetrics .
|
||||||
|
acceptOverride .
|
||||||
|
autohead .
|
||||||
|
gzip def {gzipFiles = GzipPreCompressed GzipIgnore} .
|
||||||
|
methodOverride
|
||||||
|
|
||||||
|
makeLogWare :: App -> IO Middleware
|
||||||
|
makeLogWare foundation =
|
||||||
|
mkRequestLogger
|
||||||
|
def
|
||||||
|
{ outputFormat =
|
||||||
|
if appDetailedRequestLogging (appSettings foundation)
|
||||||
|
then Detailed True
|
||||||
|
else Apache
|
||||||
|
(if appIpFromHeader (appSettings foundation)
|
||||||
|
then FromFallback
|
||||||
|
else FromSocket)
|
||||||
|
, destination = Logger (loggerSet (appLogger foundation))
|
||||||
|
}
|
||||||
|
|
||||||
|
-- | Warp settings for the given foundation value.
|
||||||
|
warpSettings :: App -> Settings
|
||||||
|
warpSettings foundation =
|
||||||
|
setPort (appPort (appSettings foundation)) $
|
||||||
|
setHost (appHost (appSettings foundation)) $
|
||||||
|
setOnException
|
||||||
|
(\_req e ->
|
||||||
|
when (defaultShouldDisplayException e) $
|
||||||
|
messageLoggerSource
|
||||||
|
foundation
|
||||||
|
(appLogger foundation)
|
||||||
|
$(qLocation >>= liftLoc)
|
||||||
|
"yesod"
|
||||||
|
LevelError
|
||||||
|
(toLogStr $ "Exception from Warp: " ++ show e))
|
||||||
|
defaultSettings
|
||||||
|
|
||||||
|
-- | For yesod devel, return the Warp settings and WAI Application.
|
||||||
|
getApplicationDev :: IO (Settings, Application)
|
||||||
|
getApplicationDev = do
|
||||||
|
settings <- getAppSettings
|
||||||
|
foundation <- makeFoundation settings
|
||||||
|
wsettings <- getDevSettings (warpSettings foundation)
|
||||||
|
app <- makeApplication foundation
|
||||||
|
forkEKG foundation
|
||||||
|
return (wsettings, app)
|
||||||
|
|
||||||
|
getAppSettings :: IO AppSettings
|
||||||
|
getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv
|
||||||
|
|
||||||
|
-- | main function for use by yesod devel
|
||||||
|
develMain :: IO ()
|
||||||
|
develMain = develMainHelper getApplicationDev
|
||||||
|
|
||||||
|
forkEKG :: App -> IO ()
|
||||||
|
forkEKG foundation =
|
||||||
|
let settings = appSettings foundation in
|
||||||
|
for_ (appEkgHost settings) $ \ekgHost ->
|
||||||
|
for_ (appEkgPort settings) $ \ekgPort ->
|
||||||
|
EKG.forkServerWith
|
||||||
|
(appMetrics foundation ^. MM.metricsStore)
|
||||||
|
(encodeUtf8 ekgHost)
|
||||||
|
ekgPort
|
||||||
|
|
||||||
|
-- | The @main@ function for an executable running this site.
|
||||||
|
appMain :: IO ()
|
||||||
|
appMain = do
|
||||||
|
settings <- loadYamlSettingsArgs [configSettingsYmlValue] useEnv
|
||||||
|
foundation <- makeFoundation settings
|
||||||
|
app <- makeApplication foundation
|
||||||
|
forkEKG foundation
|
||||||
|
runSettings (warpSettings foundation) app
|
||||||
|
|
||||||
|
getApplicationRepl :: IO (Int, App, Application)
|
||||||
|
getApplicationRepl = do
|
||||||
|
settings <- getAppSettings
|
||||||
|
foundation <- makeFoundation settings
|
||||||
|
wsettings <- getDevSettings (warpSettings foundation)
|
||||||
|
app1 <- makeApplication foundation
|
||||||
|
return (getPort wsettings, foundation, app1)
|
||||||
|
|
||||||
|
shutdownApp :: App -> IO ()
|
||||||
|
shutdownApp _ = return ()
|
||||||
|
|
||||||
|
-- | Run a handler
|
||||||
|
handler :: Handler a -> IO a
|
||||||
|
handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h
|
||||||
|
|
||||||
|
-- | Run DB queries
|
||||||
|
db :: ReaderT SqlBackend (HandlerFor App) a -> IO a
|
||||||
|
db = handler . runDB
|
250
src/Foundation.hs
Normal file
250
src/Foundation.hs
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE StandaloneDeriving #-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||||
|
|
||||||
|
module Foundation where
|
||||||
|
|
||||||
|
import Import.NoFoundation
|
||||||
|
import Database.Persist.Sql (ConnectionPool, runSqlPool)
|
||||||
|
import Text.Hamlet (hamletFile)
|
||||||
|
import Text.Jasmine (minifym)
|
||||||
|
import PathPiece()
|
||||||
|
|
||||||
|
-- import Yesod.Auth.Dummy
|
||||||
|
|
||||||
|
import Yesod.Default.Util (addStaticContentExternal)
|
||||||
|
import Yesod.Core.Types
|
||||||
|
import Yesod.Auth.Message
|
||||||
|
import qualified Network.Wai as NW
|
||||||
|
import qualified Control.Monad.Metrics as MM
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
import qualified Data.Text.Encoding as TE
|
||||||
|
import qualified Yesod.Core.Unsafe as Unsafe
|
||||||
|
|
||||||
|
data App = App
|
||||||
|
{ appSettings :: AppSettings
|
||||||
|
, appStatic :: Static -- ^ Settings for static file serving.
|
||||||
|
, appConnPool :: ConnectionPool -- ^ Database connection pool.
|
||||||
|
, appHttpManager :: Manager
|
||||||
|
, appLogger :: Logger
|
||||||
|
, appMetrics :: !MM.Metrics
|
||||||
|
} deriving (Typeable)
|
||||||
|
|
||||||
|
mkYesodData "App" $(parseRoutesFile "config/routes")
|
||||||
|
|
||||||
|
deriving instance Typeable Route
|
||||||
|
deriving instance Generic (Route App)
|
||||||
|
|
||||||
|
-- YesodPersist
|
||||||
|
|
||||||
|
instance YesodPersist App where
|
||||||
|
type YesodPersistBackend App = SqlBackend
|
||||||
|
runDB action = do
|
||||||
|
master <- getYesod
|
||||||
|
runSqlPool action (appConnPool master)
|
||||||
|
|
||||||
|
instance YesodPersistRunner App where
|
||||||
|
getDBRunner = defaultGetDBRunner appConnPool
|
||||||
|
|
||||||
|
-- Yesod
|
||||||
|
|
||||||
|
instance Yesod App where
|
||||||
|
approot = ApprootRequest $ \app req ->
|
||||||
|
case appRoot (appSettings app) of
|
||||||
|
Nothing -> getApprootText guessApproot app req
|
||||||
|
Just root -> root
|
||||||
|
|
||||||
|
makeSessionBackend _ = Just <$> defaultClientSessionBackend
|
||||||
|
10080 -- min (7 days)
|
||||||
|
"config/client_session_key.aes"
|
||||||
|
|
||||||
|
yesodMiddleware = metricsMiddleware . defaultYesodMiddleware . defaultCsrfMiddleware
|
||||||
|
|
||||||
|
defaultLayout widget = do
|
||||||
|
req <- getRequest
|
||||||
|
master <- getYesod
|
||||||
|
urlrender <- getUrlRender
|
||||||
|
mmsg <- getMessage
|
||||||
|
musername <- maybeAuthUsername
|
||||||
|
muser <- (fmap.fmap) snd maybeAuthPair
|
||||||
|
mcurrentRoute <- getCurrentRoute
|
||||||
|
void $ mapM (incrementRouteEKG req) mcurrentRoute
|
||||||
|
pc <- widgetToPageContent $ do
|
||||||
|
setTitle "Espial"
|
||||||
|
addAppScripts
|
||||||
|
addStylesheet (StaticR css_tachyons_min_css)
|
||||||
|
addStylesheet (StaticR css_main_css)
|
||||||
|
$(widgetFile "default-layout")
|
||||||
|
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
||||||
|
|
||||||
|
addStaticContent ext mime content = do
|
||||||
|
master <- getYesod
|
||||||
|
let staticDir = appStaticDir (appSettings master)
|
||||||
|
addStaticContentExternal
|
||||||
|
minifym
|
||||||
|
genFileName
|
||||||
|
staticDir
|
||||||
|
(StaticR . flip StaticRoute [])
|
||||||
|
ext
|
||||||
|
mime
|
||||||
|
content
|
||||||
|
where
|
||||||
|
genFileName lbs = "autogen-" ++ base64md5 lbs
|
||||||
|
|
||||||
|
shouldLogIO app _source level =
|
||||||
|
pure $ appShouldLogAll (appSettings app) || level == LevelWarn || level == LevelError
|
||||||
|
makeLogger = return . appLogger
|
||||||
|
|
||||||
|
authRoute _ = Just (AuthR LoginR)
|
||||||
|
|
||||||
|
isAuthorized (AuthR _) _ = pure Authorized
|
||||||
|
isAuthorized _ _ = pure Authorized
|
||||||
|
|
||||||
|
defaultMessageWidget title body = do
|
||||||
|
setTitle title
|
||||||
|
toWidget [hamlet|
|
||||||
|
<main .pv2.ph3.mh1>
|
||||||
|
<div .w-100.mw8.center>
|
||||||
|
<div .pa3.bg-near-white>
|
||||||
|
<h1>#{title}
|
||||||
|
^{body}
|
||||||
|
|]
|
||||||
|
|
||||||
|
|
||||||
|
isAuthenticated :: Handler AuthResult
|
||||||
|
isAuthenticated = maybeAuthId >>= \case
|
||||||
|
Just authId -> pure Authorized
|
||||||
|
_ -> pure $ AuthenticationRequired
|
||||||
|
|
||||||
|
addAppScripts :: (MonadWidget m, HandlerSite m ~ App) => m ()
|
||||||
|
addAppScripts = do
|
||||||
|
addScript (StaticR js_moment_min_js)
|
||||||
|
addScript (StaticR js_app_min_js)
|
||||||
|
|
||||||
|
|
||||||
|
-- popupLayout
|
||||||
|
|
||||||
|
popupLayout :: Widget -> Handler Html
|
||||||
|
popupLayout widget = do
|
||||||
|
req <- getRequest
|
||||||
|
master <- getYesod
|
||||||
|
mmsg <- getMessage
|
||||||
|
musername <- maybeAuthUsername
|
||||||
|
pc <- widgetToPageContent $ do
|
||||||
|
addAppScripts
|
||||||
|
addStylesheet (StaticR css_tachyons_min_css)
|
||||||
|
addStylesheet (StaticR css_popup_css)
|
||||||
|
$(widgetFile "popup-layout")
|
||||||
|
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
||||||
|
|
||||||
|
|
||||||
|
metricsMiddleware :: Handler a -> Handler a
|
||||||
|
metricsMiddleware handler = do
|
||||||
|
req <- getRequest
|
||||||
|
mcurrentRoute <- getCurrentRoute
|
||||||
|
void $ mapM (incrementRouteEKG req) mcurrentRoute
|
||||||
|
handler
|
||||||
|
|
||||||
|
|
||||||
|
incrementRouteEKG :: YesodRequest -> Route App -> Handler ()
|
||||||
|
incrementRouteEKG req = MM.increment . (\r -> "route." <> r <> "." <> method) . pack . constrName
|
||||||
|
where method = decodeUtf8 $ NW.requestMethod $ reqWaiRequest req
|
||||||
|
|
||||||
|
-- YesodAuth
|
||||||
|
|
||||||
|
instance YesodAuth App where
|
||||||
|
type AuthId App = UserId
|
||||||
|
-- authHttpManager = getHttpManager
|
||||||
|
authPlugins _ = [dbAuthPlugin]
|
||||||
|
authenticate = authenticateCreds
|
||||||
|
loginDest = const HomeR
|
||||||
|
logoutDest = const HomeR
|
||||||
|
onLogin = maybeAuth >>= \case
|
||||||
|
Nothing -> cpprint ("onLogin: could not find user" :: Text)
|
||||||
|
Just (Entity _ uname) -> setSession userNameKey (userName uname)
|
||||||
|
onLogout =
|
||||||
|
deleteSession userNameKey
|
||||||
|
redirectToReferer = const True
|
||||||
|
|
||||||
|
instance YesodAuthPersist App
|
||||||
|
|
||||||
|
instance MM.MonadMetrics Handler where
|
||||||
|
getMetrics = pure . appMetrics =<< getYesod
|
||||||
|
|
||||||
|
-- session keys
|
||||||
|
|
||||||
|
maybeAuthUsername :: Handler (Maybe Text)
|
||||||
|
maybeAuthUsername = do
|
||||||
|
lookupSession userNameKey
|
||||||
|
|
||||||
|
ultDestKey :: Text
|
||||||
|
ultDestKey = "_ULT"
|
||||||
|
|
||||||
|
userNameKey :: Text
|
||||||
|
userNameKey = "_UNAME"
|
||||||
|
|
||||||
|
-- dbAuthPlugin
|
||||||
|
|
||||||
|
dbAuthPluginName :: Text
|
||||||
|
dbAuthPluginName = "db"
|
||||||
|
|
||||||
|
dbAuthPlugin :: AuthPlugin App
|
||||||
|
dbAuthPlugin = AuthPlugin dbAuthPluginName dbDispatch dbLoginHandler
|
||||||
|
where
|
||||||
|
dbDispatch "POST" ["login"] = dbPostLoginR >>= sendResponse
|
||||||
|
dbDispatch _ _ = notFound
|
||||||
|
dbLoginHandler toParent = do
|
||||||
|
req <- getRequest
|
||||||
|
lookupSession ultDestKey >>= \case
|
||||||
|
Just dest | "logout" `isInfixOf` dest -> deleteSession ultDestKey
|
||||||
|
_ -> pure ()
|
||||||
|
setTitle "Espial | Log In"
|
||||||
|
$(widgetFile "login")
|
||||||
|
|
||||||
|
dbLoginR :: AuthRoute
|
||||||
|
dbLoginR = PluginR dbAuthPluginName ["login"]
|
||||||
|
|
||||||
|
dbPostLoginR :: AuthHandler master TypedContent
|
||||||
|
dbPostLoginR = do
|
||||||
|
mresult <- runInputPostResult (dbLoginCreds
|
||||||
|
<$> ireq textField "username"
|
||||||
|
<*> ireq textField "password")
|
||||||
|
case mresult of
|
||||||
|
FormSuccess creds -> setCredsRedirect creds
|
||||||
|
_ -> loginErrorMessageI LoginR InvalidUsernamePass
|
||||||
|
|
||||||
|
dbLoginCreds :: Text -> Text -> Creds master
|
||||||
|
dbLoginCreds username password =
|
||||||
|
Creds
|
||||||
|
{ credsPlugin = dbAuthPluginName
|
||||||
|
, credsIdent = username
|
||||||
|
, credsExtra = [("password", password)]
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticateCreds ::
|
||||||
|
(MonadHandler m, HandlerSite m ~ App)
|
||||||
|
=> Creds App
|
||||||
|
-> m (AuthenticationResult App)
|
||||||
|
authenticateCreds Creds {..} = do
|
||||||
|
muser <-
|
||||||
|
case credsPlugin of
|
||||||
|
p | p == dbAuthPluginName -> liftHandler $ runDB $
|
||||||
|
join <$> mapM (authenticatePassword credsIdent) (lookup "password" credsExtra)
|
||||||
|
_ -> pure Nothing
|
||||||
|
case muser of
|
||||||
|
Nothing -> pure (UserError InvalidUsernamePass)
|
||||||
|
Just (Entity uid _) -> pure (Authenticated uid)
|
||||||
|
|
||||||
|
-- Util
|
||||||
|
|
||||||
|
instance RenderMessage App FormMessage where
|
||||||
|
renderMessage :: App -> [Lang] -> FormMessage -> Text
|
||||||
|
renderMessage _ _ = defaultFormMessage
|
||||||
|
|
||||||
|
instance HasHttpManager App where
|
||||||
|
getHttpManager :: App -> Manager
|
||||||
|
getHttpManager = appHttpManager
|
||||||
|
|
||||||
|
unsafeHandler :: App -> Handler a -> IO a
|
||||||
|
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
|
||||||
|
|
20
src/Generic.hs
Normal file
20
src/Generic.hs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
module Generic where
|
||||||
|
|
||||||
|
import GHC.Generics
|
||||||
|
import ClassyPrelude.Yesod
|
||||||
|
|
||||||
|
constrName :: (HasConstructor (Rep a), Generic a)=> a -> String
|
||||||
|
constrName = genericConstrName . from
|
||||||
|
|
||||||
|
class HasConstructor (f :: * -> *) where
|
||||||
|
genericConstrName :: f x -> String
|
||||||
|
|
||||||
|
instance HasConstructor f => HasConstructor (D1 c f) where
|
||||||
|
genericConstrName (M1 x) = genericConstrName x
|
||||||
|
|
||||||
|
instance (HasConstructor x, HasConstructor y) => HasConstructor (x :+: y) where
|
||||||
|
genericConstrName (L1 l) = genericConstrName l
|
||||||
|
genericConstrName (R1 r) = genericConstrName r
|
||||||
|
|
||||||
|
instance Constructor c => HasConstructor (C1 c f) where
|
||||||
|
genericConstrName x = conName x
|
50
src/Handler/AccountSettings.hs
Normal file
50
src/Handler/AccountSettings.hs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
module Handler.AccountSettings where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import qualified ClassyPrelude.Yesod as CP
|
||||||
|
|
||||||
|
getAccountSettingsR :: Handler Html
|
||||||
|
getAccountSettingsR = do
|
||||||
|
(_, user) <- requireAuthPair
|
||||||
|
let accountSettingsEl = "accountSettings" :: Text
|
||||||
|
let accountSettings = toAccountSettingsForm user
|
||||||
|
defaultLayout $ do
|
||||||
|
$(widgetFile "user-settings")
|
||||||
|
toWidgetBody [julius|
|
||||||
|
app.userR = "@{UserR (UserNameP $ userName user)}";
|
||||||
|
app.dat.accountSettings = #{ toJSON accountSettings } || [];
|
||||||
|
|]
|
||||||
|
toWidget [julius|
|
||||||
|
PS['Main'].renderAccountSettings('##{rawJS accountSettingsEl}')(app.dat.accountSettings)();
|
||||||
|
|]
|
||||||
|
|
||||||
|
postEditAccountSettingsR :: Handler ()
|
||||||
|
postEditAccountSettingsR = do
|
||||||
|
userId <- requireAuthId
|
||||||
|
accountSettingsForm <- requireCheckJsonBody
|
||||||
|
runDB (updateUserFromAccountSettingsForm userId accountSettingsForm)
|
||||||
|
|
||||||
|
|
||||||
|
getChangePasswordR :: Handler Html
|
||||||
|
getChangePasswordR = do
|
||||||
|
void requireAuthId
|
||||||
|
req <- getRequest
|
||||||
|
defaultLayout $
|
||||||
|
$(widgetFile "change-password")
|
||||||
|
|
||||||
|
postChangePasswordR :: Handler Html
|
||||||
|
postChangePasswordR = do
|
||||||
|
userId <- requireAuthId
|
||||||
|
mauthuname <- maybeAuthUsername
|
||||||
|
mresult <- runInputPostResult ((,) <$> ireq textField "oldpassword" <*> ireq textField "newpassword")
|
||||||
|
case (mauthuname, mresult) of
|
||||||
|
(Just uname, FormSuccess (old, new)) -> do
|
||||||
|
muser <- runDB (authenticatePassword uname old)
|
||||||
|
case muser of
|
||||||
|
Just _ -> do
|
||||||
|
new' <- liftIO (hashPassword new)
|
||||||
|
void $ runDB (update userId [UserPasswordHash CP.=. new'])
|
||||||
|
setMessage "Password Changed Successfully"
|
||||||
|
_ -> setMessage "Incorrect Old Password"
|
||||||
|
_ -> setMessage "Missing Required Fields"
|
||||||
|
redirect ChangePasswordR
|
67
src/Handler/Add.hs
Normal file
67
src/Handler/Add.hs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
module Handler.Add where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Archive
|
||||||
|
import Data.List (nub)
|
||||||
|
|
||||||
|
-- View
|
||||||
|
|
||||||
|
getAddViewR :: Handler Html
|
||||||
|
getAddViewR = do
|
||||||
|
userId <- requireAuthId
|
||||||
|
|
||||||
|
murl <- lookupGetParam "url"
|
||||||
|
mformdb <- runDB (pure . fmap _toBookmarkForm =<< fetchBookmarkByUrl userId murl)
|
||||||
|
formurl <- bookmarkFormUrl
|
||||||
|
|
||||||
|
let renderEl = "addForm" :: Text
|
||||||
|
|
||||||
|
popupLayout $ do
|
||||||
|
toWidget [whamlet|
|
||||||
|
<div id="#{ renderEl }">
|
||||||
|
|]
|
||||||
|
toWidgetBody [julius|
|
||||||
|
app.dat.bmark = #{ toJSON (fromMaybe formurl mformdb) };
|
||||||
|
|]
|
||||||
|
toWidget [julius|
|
||||||
|
PS['Main'].renderAddForm('##{rawJS renderEl}')(app.dat.bmark)();
|
||||||
|
|]
|
||||||
|
|
||||||
|
bookmarkFormUrl :: Handler BookmarkForm
|
||||||
|
bookmarkFormUrl = do
|
||||||
|
Entity _ user <- requireAuth
|
||||||
|
BookmarkForm
|
||||||
|
<$> (lookupGetParam "url" >>= pure . fromMaybe "")
|
||||||
|
<*> (lookupGetParam "title")
|
||||||
|
<*> (lookupGetParam "description" >>= pure . fmap Textarea)
|
||||||
|
<*> (lookupGetParam "tags")
|
||||||
|
<*> (lookupGetParam "private" >>= pure . fmap parseChk <&> (<|> Just (userPrivateDefault user)))
|
||||||
|
<*> (lookupGetParam "toread" >>= pure . fmap parseChk)
|
||||||
|
<*> pure Nothing
|
||||||
|
<*> pure Nothing
|
||||||
|
<*> pure Nothing
|
||||||
|
<*> pure Nothing
|
||||||
|
<*> pure Nothing
|
||||||
|
where
|
||||||
|
parseChk s = s == "yes" || s == "on"
|
||||||
|
|
||||||
|
-- API
|
||||||
|
|
||||||
|
postAddR :: Handler ()
|
||||||
|
postAddR = do
|
||||||
|
bookmarkForm <- requireCheckJsonBody
|
||||||
|
_handleFormSuccess bookmarkForm >>= \case
|
||||||
|
(Created, bid) -> sendStatusJSON created201 bid
|
||||||
|
(Updated, _) -> sendResponseStatus noContent204 ()
|
||||||
|
|
||||||
|
_handleFormSuccess :: BookmarkForm -> Handler (UpsertResult, Key Bookmark)
|
||||||
|
_handleFormSuccess bookmarkForm = do
|
||||||
|
(userId, user) <- requireAuthPair
|
||||||
|
bm <- liftIO $ _toBookmark userId bookmarkForm
|
||||||
|
(res, kbid) <- runDB (upsertBookmark mkbid bm tags)
|
||||||
|
whenM (shouldArchiveBookmark user kbid) $
|
||||||
|
void $ async (archiveBookmarkUrl kbid (unpack (bookmarkHref bm)))
|
||||||
|
pure (res, kbid)
|
||||||
|
where
|
||||||
|
mkbid = BookmarkKey <$> _bid bookmarkForm
|
||||||
|
tags = maybe [] (nub . words) (_tags bookmarkForm)
|
106
src/Handler/Archive.hs
Normal file
106
src/Handler/Archive.hs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
module Handler.Archive where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Data.Function ((&))
|
||||||
|
import qualified Data.Attoparsec.ByteString.Char8 as AP
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
import qualified Data.ByteString.Char8 as BS8
|
||||||
|
import qualified Network.HTTP.Client as NH
|
||||||
|
import qualified Network.HTTP.Client.TLS as NH
|
||||||
|
import qualified Network.HTTP.Types.Status as NH
|
||||||
|
import qualified Web.FormUrlEncoded as WH
|
||||||
|
import qualified Control.Monad.Metrics as MM
|
||||||
|
|
||||||
|
shouldArchiveBookmark :: User -> Key Bookmark -> Handler Bool
|
||||||
|
shouldArchiveBookmark user kbid = do
|
||||||
|
runDB (get kbid) >>= \case
|
||||||
|
Nothing -> pure False
|
||||||
|
Just bm -> do
|
||||||
|
pure $
|
||||||
|
(isNothing $ bookmarkArchiveHref bm) &&
|
||||||
|
(bookmarkShared bm)
|
||||||
|
&& not (_isArchiveBlacklisted bm)
|
||||||
|
&& not (userPrivacyLock user)
|
||||||
|
&& userArchiveDefault user
|
||||||
|
|
||||||
|
archiveBookmarkUrl :: Key Bookmark -> String -> Handler ()
|
||||||
|
archiveBookmarkUrl kbid url =
|
||||||
|
(_fetchArchiveSubmitInfo >>= \case
|
||||||
|
Left e -> do
|
||||||
|
MM.increment "archive.fetchSubmitId_noparse"
|
||||||
|
$(logError) (pack e)
|
||||||
|
Right submitInfo -> do
|
||||||
|
userId <- requireAuthId
|
||||||
|
let req = _buildArchiveSubmitRequest submitInfo url
|
||||||
|
MM.increment "archive.submit"
|
||||||
|
res <- liftIO $ NH.httpLbs req =<< NH.getGlobalManager
|
||||||
|
let status = NH.responseStatus res
|
||||||
|
MM.increment ("archive.submit_status_" <> (pack.show) (NH.statusCode status))
|
||||||
|
let updateArchiveUrl = runDB . updateBookmarkArchiveUrl userId kbid . Just
|
||||||
|
headers = NH.responseHeaders res
|
||||||
|
case status of
|
||||||
|
s | s == NH.status200 ->
|
||||||
|
for_ (lookup "Refresh" headers >>= _parseRefreshHeaderUrl) updateArchiveUrl
|
||||||
|
s | s == NH.status302 ->
|
||||||
|
for_ (lookup "Location" headers) (updateArchiveUrl . decodeUtf8)
|
||||||
|
_ -> $(logError) (pack (show res)))
|
||||||
|
`catch` (\(e::SomeException) -> ($(logError) $ (pack.show) e) >> throwIO e)
|
||||||
|
|
||||||
|
_isArchiveBlacklisted :: Bookmark -> Bool
|
||||||
|
_isArchiveBlacklisted (Bookmark {..}) =
|
||||||
|
[ "hulu"
|
||||||
|
, "livestream"
|
||||||
|
, "netflix"
|
||||||
|
, "skillsmatter"
|
||||||
|
, "twitch.tv"
|
||||||
|
, "vimeo"
|
||||||
|
, "youtu.be"
|
||||||
|
, "youtube"
|
||||||
|
, "archive."
|
||||||
|
] &
|
||||||
|
any (`isInfixOf` bookmarkHref)
|
||||||
|
|
||||||
|
_parseRefreshHeaderUrl :: ByteString -> Maybe Text
|
||||||
|
_parseRefreshHeaderUrl h = do
|
||||||
|
let u = BS8.drop 1 $ BS8.dropWhile (/= '=') h
|
||||||
|
if (not (null u))
|
||||||
|
then Just $ decodeUtf8 u
|
||||||
|
else Nothing
|
||||||
|
|
||||||
|
_buildArchiveSubmitRequest :: (String, String) -> String -> NH.Request
|
||||||
|
_buildArchiveSubmitRequest (action, submitId) href =
|
||||||
|
NH.parseRequest_ ("POST " <> action) & \r ->
|
||||||
|
r { NH.requestHeaders =
|
||||||
|
[ ("User-Agent", _archiveUserAgent)
|
||||||
|
, ("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
]
|
||||||
|
, NH.requestBody = NH.RequestBodyLBS $ WH.urlEncodeAsForm ((
|
||||||
|
[ ("submitid" , submitId)
|
||||||
|
, ("url", href)
|
||||||
|
]) :: [(String, String)])
|
||||||
|
, NH.redirectCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_fetchArchiveSubmitInfo :: Handler (Either String (String , String))
|
||||||
|
_fetchArchiveSubmitInfo = do
|
||||||
|
MM.increment "archive.fetchSubmitId"
|
||||||
|
res <- liftIO $ NH.httpLbs buildSubmitRequest =<< NH.getGlobalManager
|
||||||
|
MM.increment ("archive.fetchSubmitId_status_" <> (pack.show) (NH.statusCode (NH.responseStatus res)))
|
||||||
|
let body = LBS.toStrict (responseBody res)
|
||||||
|
action = _parseSubstring (AP.string "action=\"") (AP.notChar '"') body
|
||||||
|
submitId = _parseSubstring (AP.string "submitid\" value=\"") (AP.notChar '"') body
|
||||||
|
pure $ (,) <$> action <*> submitId
|
||||||
|
where
|
||||||
|
buildSubmitRequest =
|
||||||
|
NH.parseRequest_ "https://archive.li/" & \r ->
|
||||||
|
r {NH.requestHeaders = [("User-Agent", _archiveUserAgent)]}
|
||||||
|
|
||||||
|
_archiveUserAgent :: ByteString
|
||||||
|
_archiveUserAgent = "espial"
|
||||||
|
|
||||||
|
_parseSubstring :: AP.Parser ByteString -> AP.Parser Char -> BS.ByteString -> Either String String
|
||||||
|
_parseSubstring start inner res = do
|
||||||
|
(flip AP.parseOnly) res (skipAnyTill start >> AP.many1 inner)
|
||||||
|
where
|
||||||
|
skipAnyTill end = go where go = end *> pure () <|> AP.anyChar *> go
|
31
src/Handler/Common.hs
Normal file
31
src/Handler/Common.hs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
-- | Common handler functions.
|
||||||
|
module Handler.Common where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Data.FileEmbed (embedFile)
|
||||||
|
import Text.Read
|
||||||
|
|
||||||
|
-- These handlers embed files in the executable at compile time to avoid a
|
||||||
|
-- runtime dependency, and for efficiency.
|
||||||
|
|
||||||
|
getFaviconR :: Handler TypedContent
|
||||||
|
getFaviconR = do cacheSeconds $ 60 * 5
|
||||||
|
--cacheSeconds $ 60 * 60 * 24 * 30 -- cache for a month
|
||||||
|
return $ TypedContent "image/x-icon"
|
||||||
|
$ toContent $(embedFile "config/favicon.ico")
|
||||||
|
|
||||||
|
getRobotsR :: Handler TypedContent
|
||||||
|
getRobotsR = return $ TypedContent typePlain
|
||||||
|
$ toContent $(embedFile "config/robots.txt")
|
||||||
|
|
||||||
|
|
||||||
|
lookupPagingParams :: Handler (Maybe Int64, Maybe Int64)
|
||||||
|
lookupPagingParams = do
|
||||||
|
cq <- fmap parseMaybe (lookupGetParam "count")
|
||||||
|
cs <- fmap parseMaybe (lookupSession "count")
|
||||||
|
for_ cq (setSession "count" . (pack . show))
|
||||||
|
pq <- fmap parseMaybe (lookupGetParam "page")
|
||||||
|
pure (cq <|> cs, pq)
|
||||||
|
where
|
||||||
|
parseMaybe x = readMaybe . unpack =<< x
|
9
src/Handler/Docs.hs
Normal file
9
src/Handler/Docs.hs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||||
|
|
||||||
|
module Handler.Docs where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
getDocsSearchR :: Handler Html
|
||||||
|
getDocsSearchR = popupLayout $
|
||||||
|
$(widgetFile "docs-search")
|
51
src/Handler/Edit.hs
Normal file
51
src/Handler/Edit.hs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||||
|
|
||||||
|
module Handler.Edit where
|
||||||
|
|
||||||
|
import Database.Persist.Sql
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
-- routes
|
||||||
|
|
||||||
|
deleteDeleteR :: Int64 -> Handler Html
|
||||||
|
deleteDeleteR bid = do
|
||||||
|
userId <- requireAuthId
|
||||||
|
runDB $ do
|
||||||
|
let k_bid = BookmarkKey bid
|
||||||
|
_ <- requireResource userId k_bid
|
||||||
|
deleteCascade k_bid
|
||||||
|
return ""
|
||||||
|
|
||||||
|
postReadR :: Int64 -> Handler Html
|
||||||
|
postReadR bid = do
|
||||||
|
userId <- requireAuthId
|
||||||
|
runDB $ do
|
||||||
|
let k_bid = BookmarkKey bid
|
||||||
|
_ <- requireResource userId k_bid
|
||||||
|
update k_bid [BookmarkToRead =. False]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
postStarR :: Int64 -> Handler Html
|
||||||
|
postStarR bid = _setSelected bid True
|
||||||
|
|
||||||
|
postUnstarR :: Int64 -> Handler Html
|
||||||
|
postUnstarR bid = _setSelected bid False
|
||||||
|
|
||||||
|
-- common
|
||||||
|
|
||||||
|
_setSelected :: Int64 -> Bool -> Handler Html
|
||||||
|
_setSelected bid selected = do
|
||||||
|
userId <- requireAuthId
|
||||||
|
runDB $ do
|
||||||
|
let k_bid = BookmarkKey bid
|
||||||
|
bm <- requireResource userId k_bid
|
||||||
|
update k_bid [BookmarkSelected =. selected]
|
||||||
|
pure ""
|
||||||
|
|
||||||
|
requireResource :: UserId -> Key Bookmark -> DBM Handler Bookmark
|
||||||
|
requireResource userId k_bid = do
|
||||||
|
bmark <- get404 k_bid
|
||||||
|
if userId == bookmarkUserId bmark
|
||||||
|
then return bmark
|
||||||
|
else notFound
|
12
src/Handler/Home.hs
Normal file
12
src/Handler/Home.hs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||||
|
|
||||||
|
module Handler.Home where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
getHomeR :: Handler Html
|
||||||
|
getHomeR = do
|
||||||
|
musername <- maybeAuthUsername
|
||||||
|
case musername of
|
||||||
|
Nothing -> redirect (AuthR LoginR)
|
||||||
|
Just username -> redirect (UserR (UserNameP username))
|
134
src/Handler/Notes.hs
Normal file
134
src/Handler/Notes.hs
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||||
|
module Handler.Notes where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Common (lookupPagingParams)
|
||||||
|
import qualified Data.Aeson as A
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
getNotesR :: UserNameP -> Handler Html
|
||||||
|
getNotesR unamep@(UserNameP uname) = do
|
||||||
|
void requireAuthId
|
||||||
|
(limit', page') <- lookupPagingParams
|
||||||
|
let queryp = "query" :: Text
|
||||||
|
mquery <- lookupGetParam queryp
|
||||||
|
let limit = maybe 20 fromIntegral limit'
|
||||||
|
page = maybe 1 fromIntegral page'
|
||||||
|
mqueryp = fmap (\q -> (queryp, q)) mquery
|
||||||
|
(bcount, notes) <-
|
||||||
|
runDB $
|
||||||
|
do Entity userId _ <- getBy404 (UniqueUserName uname)
|
||||||
|
getNoteList userId mquery limit page
|
||||||
|
req <- getRequest
|
||||||
|
mroute <- getCurrentRoute
|
||||||
|
defaultLayout $ do
|
||||||
|
let pager = $(widgetFile "pager")
|
||||||
|
search = $(widgetFile "search")
|
||||||
|
renderEl = "notes" :: Text
|
||||||
|
$(widgetFile "notes")
|
||||||
|
toWidgetBody [julius|
|
||||||
|
app.userR = "@{UserR unamep}";
|
||||||
|
app.dat.notes = #{ toJSON notes } || [];
|
||||||
|
|]
|
||||||
|
toWidget [julius|
|
||||||
|
PS['Main'].renderNotes('##{rawJS renderEl}')(app.dat.notes)();
|
||||||
|
|]
|
||||||
|
|
||||||
|
getNoteR :: UserNameP -> NtSlug -> Handler Html
|
||||||
|
getNoteR unamep@(UserNameP uname) slug = do
|
||||||
|
void requireAuthId
|
||||||
|
let renderEl = "note" :: Text
|
||||||
|
note <-
|
||||||
|
runDB $
|
||||||
|
do Entity userId _ <- getBy404 (UniqueUserName uname)
|
||||||
|
mnote <- getNote userId slug
|
||||||
|
maybe notFound pure mnote
|
||||||
|
defaultLayout $ do
|
||||||
|
addScript (StaticR js_marked_min_js)
|
||||||
|
$(widgetFile "note")
|
||||||
|
toWidgetBody [julius|
|
||||||
|
app.userR = "@{UserR unamep}";
|
||||||
|
app.dat.note = #{ toJSON note } || [];
|
||||||
|
|]
|
||||||
|
toWidget [julius|
|
||||||
|
PS['Main'].renderNote('##{rawJS renderEl}')(app.dat.note)();
|
||||||
|
|]
|
||||||
|
|
||||||
|
getAddNoteViewR :: UserNameP -> Handler Html
|
||||||
|
getAddNoteViewR unamep@(UserNameP uname) = do
|
||||||
|
userId <- requireAuthId
|
||||||
|
let renderEl = "note" :: Text
|
||||||
|
note <- liftIO $ Entity (NoteKey 0) <$> _toNote userId (NoteForm Nothing Nothing Nothing Nothing Nothing Nothing Nothing)
|
||||||
|
defaultLayout $ do
|
||||||
|
addScript (StaticR js_marked_min_js)
|
||||||
|
$(widgetFile "note")
|
||||||
|
toWidgetBody [julius|
|
||||||
|
app.userR = "@{UserR unamep}";
|
||||||
|
app.noteR = "@{NoteR unamep (noteSlug (entityVal note))}";
|
||||||
|
app.dat.note = #{ toJSON note } || [];
|
||||||
|
|]
|
||||||
|
toWidget [julius|
|
||||||
|
PS['Main'].renderNote('##{rawJS renderEl}')(app.dat.note)();
|
||||||
|
|]
|
||||||
|
|
||||||
|
deleteDeleteNoteR :: Int64 -> Handler Html
|
||||||
|
deleteDeleteNoteR nid = do
|
||||||
|
userId <- requireAuthId
|
||||||
|
runDB $ do
|
||||||
|
let k_nid = NoteKey nid
|
||||||
|
_ <- requireResource userId k_nid
|
||||||
|
deleteCascade k_nid
|
||||||
|
return ""
|
||||||
|
|
||||||
|
postAddNoteR :: Handler ()
|
||||||
|
postAddNoteR = do
|
||||||
|
noteForm <- requireCheckJsonBody
|
||||||
|
_handleFormSuccess noteForm >>= \case
|
||||||
|
(Created, nid) -> sendStatusJSON created201 nid
|
||||||
|
(Updated, _) -> sendResponseStatus noContent204 ()
|
||||||
|
|
||||||
|
requireResource :: UserId -> Key Note -> DBM Handler Note
|
||||||
|
requireResource userId k_nid = do
|
||||||
|
nnote <- get404 k_nid
|
||||||
|
if userId == noteUserId nnote
|
||||||
|
then return nnote
|
||||||
|
else notFound
|
||||||
|
|
||||||
|
_handleFormSuccess :: NoteForm -> Handler (UpsertResult, Key Note)
|
||||||
|
_handleFormSuccess noteForm = do
|
||||||
|
userId <- requireAuthId
|
||||||
|
note <- liftIO $ _toNote userId noteForm
|
||||||
|
runDB (upsertNote knid note)
|
||||||
|
where
|
||||||
|
knid = NoteKey <$> (_id noteForm >>= \i -> if i > 0 then Just i else Nothing)
|
||||||
|
|
||||||
|
data NoteForm = NoteForm
|
||||||
|
{ _id :: Maybe Int64
|
||||||
|
, _slug :: Maybe NtSlug
|
||||||
|
, _title :: Maybe Text
|
||||||
|
, _text :: Maybe Textarea
|
||||||
|
, _isMarkdown :: Maybe Bool
|
||||||
|
, _created :: Maybe UTCTimeStr
|
||||||
|
, _updated :: Maybe UTCTimeStr
|
||||||
|
} deriving (Show, Eq, Read, Generic)
|
||||||
|
|
||||||
|
instance FromJSON NoteForm where parseJSON = A.genericParseJSON gNoteFormOptions
|
||||||
|
instance ToJSON NoteForm where toJSON = A.genericToJSON gNoteFormOptions
|
||||||
|
|
||||||
|
gNoteFormOptions :: A.Options
|
||||||
|
gNoteFormOptions = A.defaultOptions { A.fieldLabelModifier = drop 1 }
|
||||||
|
|
||||||
|
_toNote :: UserId -> NoteForm -> IO Note
|
||||||
|
_toNote userId NoteForm {..} = do
|
||||||
|
time <- liftIO getCurrentTime
|
||||||
|
slug <- maybe mkNtSlug pure _slug
|
||||||
|
pure $
|
||||||
|
Note
|
||||||
|
userId
|
||||||
|
slug
|
||||||
|
(length _text)
|
||||||
|
(fromMaybe "" _title)
|
||||||
|
(maybe "" unTextarea _text)
|
||||||
|
(fromMaybe False _isMarkdown)
|
||||||
|
(fromMaybe time (fmap unUTCTimeStr _created))
|
||||||
|
(fromMaybe time (fmap unUTCTimeStr _updated))
|
62
src/Handler/User.hs
Normal file
62
src/Handler/User.hs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||||
|
module Handler.User where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import Handler.Common (lookupPagingParams)
|
||||||
|
|
||||||
|
getUserR :: UserNameP -> Handler Html
|
||||||
|
getUserR uname@(UserNameP name) = do
|
||||||
|
_getUser uname SharedAll FilterAll (TagsP [])
|
||||||
|
|
||||||
|
getUserSharedR :: UserNameP -> SharedP -> Handler Html
|
||||||
|
getUserSharedR uname sharedp =
|
||||||
|
_getUser uname sharedp FilterAll (TagsP [])
|
||||||
|
|
||||||
|
getUserFilterR :: UserNameP -> FilterP -> Handler Html
|
||||||
|
getUserFilterR uname filterp =
|
||||||
|
_getUser uname SharedAll filterp (TagsP [])
|
||||||
|
|
||||||
|
getUserTagsR :: UserNameP -> TagsP -> Handler Html
|
||||||
|
getUserTagsR uname pathtags =
|
||||||
|
_getUser uname SharedAll FilterAll pathtags
|
||||||
|
|
||||||
|
_getUser :: UserNameP -> SharedP -> FilterP -> TagsP -> Handler Html
|
||||||
|
_getUser unamep@(UserNameP uname) sharedp' filterp' (TagsP pathtags) = do
|
||||||
|
mauthuname <- maybeAuthUsername
|
||||||
|
(limit', page') <- lookupPagingParams
|
||||||
|
let limit = maybe 120 fromIntegral limit'
|
||||||
|
page = maybe 1 fromIntegral page'
|
||||||
|
isowner = maybe False (== uname) mauthuname
|
||||||
|
sharedp = if isowner then sharedp' else SharedPublic
|
||||||
|
filterp = case filterp' of
|
||||||
|
FilterSingle _ -> filterp'
|
||||||
|
_ -> if isowner then filterp' else FilterAll
|
||||||
|
isAll = filterp == FilterAll && sharedp == SharedAll && pathtags == []
|
||||||
|
queryp = "query" :: Text
|
||||||
|
mquery <- lookupGetParam queryp
|
||||||
|
let mqueryp = fmap (\q -> (queryp, q)) mquery
|
||||||
|
(bcount, bmarks, alltags) <-
|
||||||
|
runDB $
|
||||||
|
do Entity userId user <- getBy404 (UniqueUserName uname)
|
||||||
|
when (not isowner && userPrivacyLock user)
|
||||||
|
(redirect (AuthR LoginR))
|
||||||
|
(cnt, bm) <- bookmarksQuery userId sharedp filterp pathtags mquery limit page
|
||||||
|
tg <- tagsQuery bm
|
||||||
|
pure (cnt, bm, tg)
|
||||||
|
when (bcount == 0) (case filterp of FilterSingle _ -> notFound; _ -> pure ())
|
||||||
|
mroute <- getCurrentRoute
|
||||||
|
req <- getRequest
|
||||||
|
defaultLayout $ do
|
||||||
|
let pager = $(widgetFile "pager")
|
||||||
|
search = $(widgetFile "search")
|
||||||
|
renderEl = "bookmarks" :: Text
|
||||||
|
$(widgetFile "user")
|
||||||
|
toWidgetBody [julius|
|
||||||
|
app.dat.bmarks = #{ toJSON $ toBookmarkFormList bmarks alltags } || [];
|
||||||
|
app.dat.isowner = #{ isowner };
|
||||||
|
app.userR = "@{UserR unamep}";
|
||||||
|
|]
|
||||||
|
toWidget [julius|
|
||||||
|
PS['Main'].renderBookmarks('##{rawJS renderEl}')(app.dat.bmarks)();
|
||||||
|
|]
|
86
src/Import.hs
Normal file
86
src/Import.hs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Import
|
||||||
|
( module Import
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Foundation as Import
|
||||||
|
import Import.NoFoundation as Import
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Char8 as B8
|
||||||
|
import qualified Data.Aeson as A
|
||||||
|
|
||||||
|
-- Forms
|
||||||
|
|
||||||
|
type MonadHandlerForm m = (RenderMessage App FormMessage, HandlerSite m ~ App, MonadHandler m)
|
||||||
|
|
||||||
|
type Form f = Html -> MForm Handler (FormResult f, Widget)
|
||||||
|
|
||||||
|
runInputPostJSONResult
|
||||||
|
:: (FromJSON a, MonadHandlerForm m)
|
||||||
|
=> FormInput m a -> m (FormResult a)
|
||||||
|
runInputPostJSONResult form = do
|
||||||
|
mct <- lookupHeader "content-type"
|
||||||
|
case fmap (B8.takeWhile (/= ';')) mct of
|
||||||
|
Just "application/json" ->
|
||||||
|
parseJsonBody >>= \case
|
||||||
|
A.Success a -> pure $ FormSuccess a
|
||||||
|
A.Error e -> pure $ FormFailure [pack e]
|
||||||
|
Just "application/x-www-form-urlencoded" ->
|
||||||
|
runInputPostResult form
|
||||||
|
_ -> pure FormMissing
|
||||||
|
|
||||||
|
runInputPostJSON
|
||||||
|
:: (FromJSON a, MonadHandlerForm m)
|
||||||
|
=> FormInput m a -> m a
|
||||||
|
runInputPostJSON form =
|
||||||
|
runInputPostJSONResult form >>=
|
||||||
|
\case
|
||||||
|
FormSuccess a -> pure a
|
||||||
|
FormFailure e -> invalidArgs e
|
||||||
|
FormMissing -> invalidArgs []
|
||||||
|
|
||||||
|
class MkIForm a where
|
||||||
|
mkIForm :: MonadHandlerForm m => FormInput m a
|
||||||
|
|
||||||
|
aFormToMaybeGetSuccess
|
||||||
|
:: MonadHandler f
|
||||||
|
=> AForm f a -> f (Maybe a)
|
||||||
|
aFormToMaybeGetSuccess =
|
||||||
|
fmap maybeSuccess . fmap fst . runFormGet . const . fmap fst . aFormToForm
|
||||||
|
|
||||||
|
aFormToMaybePostSuccess
|
||||||
|
:: MonadHandlerForm f
|
||||||
|
=> AForm f a -> f (Maybe a)
|
||||||
|
aFormToMaybePostSuccess =
|
||||||
|
fmap maybeSuccess . fmap fst . runFormPostNoToken . const . fmap fst . aFormToForm
|
||||||
|
|
||||||
|
maybeSuccess :: FormResult a -> Maybe a
|
||||||
|
maybeSuccess (FormSuccess a) = Just a
|
||||||
|
maybeSuccess _ = Nothing
|
||||||
|
|
||||||
|
|
||||||
|
-- FieldSettings
|
||||||
|
|
||||||
|
named :: Text -> FieldSettings master -> FieldSettings master
|
||||||
|
named n f =
|
||||||
|
f
|
||||||
|
{ fsName = Just n
|
||||||
|
, fsId = Just n
|
||||||
|
}
|
||||||
|
|
||||||
|
attr :: (Text,Text) -> FieldSettings master -> FieldSettings master
|
||||||
|
attr n f =
|
||||||
|
f
|
||||||
|
{ fsAttrs = n : fsAttrs f
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs :: [(Text,Text)] -> FieldSettings master -> FieldSettings master
|
||||||
|
attrs n f =
|
||||||
|
f
|
||||||
|
{ fsAttrs = n ++ fsAttrs f
|
||||||
|
}
|
||||||
|
|
||||||
|
cls :: [Text] -> FieldSettings master -> FieldSettings master
|
||||||
|
cls n = attrs [("class", intercalate " " n)]
|
34
src/Import/NoFoundation.hs
Normal file
34
src/Import/NoFoundation.hs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
|
||||||
|
|
||||||
|
module Import.NoFoundation
|
||||||
|
( module Import
|
||||||
|
#if MIN_VERSION_base(4, 11, 0)
|
||||||
|
#else
|
||||||
|
, (<&>)
|
||||||
|
#endif
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude.Yesod as Import
|
||||||
|
import Control.Monad.Trans.Maybe as Import
|
||||||
|
import Settings as Import
|
||||||
|
import Settings.StaticFiles as Import
|
||||||
|
import Yesod.Auth as Import
|
||||||
|
import Yesod.Core.Types as Import (loggerSet)
|
||||||
|
import Yesod.Default.Config2 as Import
|
||||||
|
import Text.Julius as Import
|
||||||
|
|
||||||
|
import Model as Import
|
||||||
|
import ModelCustom as Import
|
||||||
|
import Types as Import
|
||||||
|
import Pretty as Import
|
||||||
|
import Data.Functor as Import
|
||||||
|
import Generic as Import
|
||||||
|
|
||||||
|
|
||||||
|
#if MIN_VERSION_base(4, 11, 0)
|
||||||
|
#else
|
||||||
|
(<&>) :: Functor f => f a -> (a -> b) -> f b
|
||||||
|
as <&> f = f <$> as
|
||||||
|
infixl 1 <&>
|
||||||
|
#endif
|
569
src/Model.hs
Normal file
569
src/Model.hs
Normal file
|
@ -0,0 +1,569 @@
|
||||||
|
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
|
||||||
|
|
||||||
|
module Model where
|
||||||
|
|
||||||
|
import qualified ClassyPrelude.Yesod as CP
|
||||||
|
import qualified Data.Aeson as A
|
||||||
|
import qualified Data.Attoparsec.Text as P
|
||||||
|
import qualified Control.Monad.Combinators as PC
|
||||||
|
import qualified Data.List.NonEmpty as NE
|
||||||
|
import qualified Data.Time.ISO8601 as TI
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Data.Time as TI
|
||||||
|
import ClassyPrelude.Yesod hiding ((||.))
|
||||||
|
import Control.Monad.Trans.Maybe
|
||||||
|
import Control.Monad.Writer (tell)
|
||||||
|
import Data.Char (isSpace)
|
||||||
|
import Data.Either (fromRight)
|
||||||
|
import Data.Foldable (foldl, foldl1, sequenceA_)
|
||||||
|
import Data.List.NonEmpty (NonEmpty(..))
|
||||||
|
import Database.Esqueleto hiding ((==.))
|
||||||
|
import Pretty
|
||||||
|
import System.Directory
|
||||||
|
import Types
|
||||||
|
|
||||||
|
import ModelCustom
|
||||||
|
|
||||||
|
share [mkPersist sqlSettings, mkDeleteCascade sqlSettings, mkMigrate "migrateSchema"] [persistLowerCase|
|
||||||
|
User json
|
||||||
|
Id Int64
|
||||||
|
name Text
|
||||||
|
passwordHash BCrypt
|
||||||
|
apiToken Text Maybe
|
||||||
|
privateDefault Bool
|
||||||
|
archiveDefault Bool
|
||||||
|
privacyLock Bool
|
||||||
|
UniqueUserName name
|
||||||
|
deriving Show Eq Typeable Ord
|
||||||
|
|
||||||
|
Bookmark json
|
||||||
|
Id Int64
|
||||||
|
userId UserId
|
||||||
|
slug BmSlug default="(lower(hex(randomblob(6))))"
|
||||||
|
href Text
|
||||||
|
description Text
|
||||||
|
extended Text
|
||||||
|
time UTCTime
|
||||||
|
shared Bool
|
||||||
|
toRead Bool
|
||||||
|
selected Bool
|
||||||
|
archiveHref Text Maybe
|
||||||
|
UniqueUserHref userId href
|
||||||
|
UniqueUserSlug userId slug
|
||||||
|
deriving Show Eq Typeable Ord
|
||||||
|
|
||||||
|
BookmarkTag json
|
||||||
|
Id Int64
|
||||||
|
userId UserId
|
||||||
|
tag Text
|
||||||
|
bookmarkId BookmarkId
|
||||||
|
seq Int
|
||||||
|
UniqueUserTagBookmarkId userId tag bookmarkId
|
||||||
|
UniqueUserBookmarkIdTagSeq userId bookmarkId tag seq
|
||||||
|
deriving Show Eq Typeable Ord
|
||||||
|
|
||||||
|
Note json
|
||||||
|
Id Int64
|
||||||
|
userId UserId
|
||||||
|
slug NtSlug default="(lower(hex(randomblob(10))))"
|
||||||
|
length Int
|
||||||
|
title Text
|
||||||
|
text Text
|
||||||
|
isMarkdown Bool
|
||||||
|
created UTCTime
|
||||||
|
updated UTCTime
|
||||||
|
deriving Show Eq Typeable Ord
|
||||||
|
|]
|
||||||
|
|
||||||
|
newtype UTCTimeStr =
|
||||||
|
UTCTimeStr { unUTCTimeStr :: UTCTime }
|
||||||
|
deriving (Eq, Show, Read, Generic, FromJSON, ToJSON)
|
||||||
|
|
||||||
|
instance PathPiece UTCTimeStr where
|
||||||
|
toPathPiece (UTCTimeStr u) = pack (TI.formatISO8601Millis u)
|
||||||
|
fromPathPiece s = UTCTimeStr <$> TI.parseISO8601 (unpack s)
|
||||||
|
|
||||||
|
newtype UserNameP =
|
||||||
|
UserNameP { unUserNameP :: Text }
|
||||||
|
deriving (Eq, Show, Read)
|
||||||
|
|
||||||
|
newtype TagsP =
|
||||||
|
TagsP { unTagsP :: [Text] }
|
||||||
|
deriving (Eq, Show, Read)
|
||||||
|
|
||||||
|
data SharedP
|
||||||
|
= SharedAll
|
||||||
|
| SharedPublic
|
||||||
|
| SharedPrivate
|
||||||
|
deriving (Eq, Show, Read)
|
||||||
|
|
||||||
|
data FilterP
|
||||||
|
= FilterAll
|
||||||
|
| FilterUnread
|
||||||
|
| FilterUntagged
|
||||||
|
| FilterStarred
|
||||||
|
| FilterSingle BmSlug
|
||||||
|
deriving (Eq, Show, Read)
|
||||||
|
|
||||||
|
newtype UnreadOnly =
|
||||||
|
UnreadOnly { unUnreadOnly :: Bool }
|
||||||
|
deriving (Eq, Show, Read)
|
||||||
|
|
||||||
|
type Limit = Int64
|
||||||
|
type Page = Int64
|
||||||
|
|
||||||
|
migrateAll :: Migration
|
||||||
|
migrateAll = migrateSchema >> migrateIndexes
|
||||||
|
|
||||||
|
dumpMigration :: DB ()
|
||||||
|
dumpMigration = printMigration migrateAll
|
||||||
|
|
||||||
|
runMigrations :: DB ()
|
||||||
|
runMigrations = runMigration migrateAll
|
||||||
|
|
||||||
|
toMigration :: [Text] -> Migration
|
||||||
|
toMigration = lift . tell . fmap (False ,)
|
||||||
|
|
||||||
|
migrateIndexes :: Migration
|
||||||
|
migrateIndexes =
|
||||||
|
toMigration
|
||||||
|
[ "CREATE INDEX IF NOT EXISTS idx_bookmark_time ON bookmark (user_id, time DESC)"
|
||||||
|
, "CREATE INDEX IF NOT EXISTS idx_bookmark_tag_bookmark_id ON bookmark_tag (bookmark_id, id, tag, seq)"
|
||||||
|
, "CREATE INDEX IF NOT EXISTS idx_note_user_created ON note (user_id, created DESC)"
|
||||||
|
]
|
||||||
|
|
||||||
|
authenticatePassword :: Text -> Text -> DB (Maybe (Entity User))
|
||||||
|
authenticatePassword username password = do
|
||||||
|
muser <- getBy (UniqueUserName username)
|
||||||
|
case muser of
|
||||||
|
Nothing -> return Nothing
|
||||||
|
Just dbuser ->
|
||||||
|
if validatePasswordHash (userPasswordHash (entityVal dbuser)) password
|
||||||
|
then return (Just dbuser)
|
||||||
|
else return Nothing
|
||||||
|
|
||||||
|
getUserByName :: UserNameP -> DB (Maybe (Entity User))
|
||||||
|
getUserByName (UserNameP uname) = do
|
||||||
|
selectFirst [UserName ==. uname] []
|
||||||
|
|
||||||
|
bookmarksQuery
|
||||||
|
:: Key User
|
||||||
|
-> SharedP
|
||||||
|
-> FilterP
|
||||||
|
-> [Tag]
|
||||||
|
-> Maybe Text
|
||||||
|
-> Limit
|
||||||
|
-> Page
|
||||||
|
-> DB (Int, [Entity Bookmark])
|
||||||
|
bookmarksQuery userId sharedp filterp tags mquery limit' page =
|
||||||
|
(,) -- total count
|
||||||
|
<$> fmap (sum . fmap E.unValue)
|
||||||
|
(select $
|
||||||
|
from $ \b -> do
|
||||||
|
_whereClause b
|
||||||
|
pure $ E.countRows)
|
||||||
|
-- paged data
|
||||||
|
<*> (select $
|
||||||
|
from $ \b -> do
|
||||||
|
_whereClause b
|
||||||
|
orderBy [desc (b ^. BookmarkTime)]
|
||||||
|
limit limit'
|
||||||
|
offset ((page - 1) * limit')
|
||||||
|
pure b)
|
||||||
|
where
|
||||||
|
_whereClause b = do
|
||||||
|
where_ $
|
||||||
|
foldl (\expr tag ->
|
||||||
|
expr &&. (exists $ -- each tag becomes an exists constraint
|
||||||
|
from $ \t ->
|
||||||
|
where_ (t ^. BookmarkTagBookmarkId E.==. b ^. BookmarkId &&.
|
||||||
|
(t ^. BookmarkTagTag `E.like` val tag))))
|
||||||
|
(b ^. BookmarkUserId E.==. val userId)
|
||||||
|
tags
|
||||||
|
case sharedp of
|
||||||
|
SharedAll -> pure ()
|
||||||
|
SharedPublic -> where_ (b ^. BookmarkShared E.==. val True)
|
||||||
|
SharedPrivate -> where_ (b ^. BookmarkShared E.==. val False)
|
||||||
|
case filterp of
|
||||||
|
FilterAll -> pure ()
|
||||||
|
FilterUnread -> where_ (b ^. BookmarkToRead E.==. val True)
|
||||||
|
FilterStarred -> where_ (b ^. BookmarkSelected E.==. val True)
|
||||||
|
FilterSingle slug -> where_ (b ^. BookmarkSlug E.==. val slug)
|
||||||
|
FilterUntagged -> where_ $ notExists $ from (\t -> where_ $
|
||||||
|
(t ^. BookmarkTagBookmarkId E.==. b ^. BookmarkId))
|
||||||
|
-- search
|
||||||
|
sequenceA_ (parseSearchQuery (toLikeExpr b) =<< mquery)
|
||||||
|
|
||||||
|
toLikeExpr :: E.SqlExpr (Entity Bookmark) -> Text -> E.SqlExpr (E.Value Bool)
|
||||||
|
toLikeExpr b term = fromRight p_allFields (P.parseOnly p_onefield term)
|
||||||
|
where
|
||||||
|
wild s = (E.%) ++. val s ++. (E.%)
|
||||||
|
toLikeB field s = b ^. field `E.like` wild s
|
||||||
|
p_allFields =
|
||||||
|
(toLikeB BookmarkHref term) ||.
|
||||||
|
(toLikeB BookmarkDescription term) ||.
|
||||||
|
(toLikeB BookmarkExtended term) ||.
|
||||||
|
(exists $ from (\t -> where_ $
|
||||||
|
(t ^. BookmarkTagBookmarkId E.==. b ^. BookmarkId) &&.
|
||||||
|
(t ^. BookmarkTagTag `E.like` (wild term))))
|
||||||
|
p_onefield = p_url <|> p_title <|> p_description <|> p_tags <|> p_after <|> p_before
|
||||||
|
where
|
||||||
|
p_url = "url:" *> fmap (toLikeB BookmarkHref) P.takeText
|
||||||
|
p_title = "title:" *> fmap (toLikeB BookmarkDescription) P.takeText
|
||||||
|
p_description = "description:" *> fmap (toLikeB BookmarkExtended) P.takeText
|
||||||
|
p_tags = "tags:" *> fmap (\term' -> exists $ from (\t -> where_ $
|
||||||
|
(t ^. BookmarkTagBookmarkId E.==. b ^. BookmarkId) &&.
|
||||||
|
(t ^. BookmarkTagTag `E.like` wild term'))) P.takeText
|
||||||
|
p_after = "after:" *> fmap ((b ^. BookmarkTime E.>=.) . val) (parseTimeText =<< P.takeText)
|
||||||
|
p_before = "before:" *> fmap ((b ^. BookmarkTime E.<=.) . val) (parseTimeText =<< P.takeText)
|
||||||
|
|
||||||
|
parseSearchQuery ::
|
||||||
|
(Text -> E.SqlExpr (E.Value Bool))
|
||||||
|
-> Text
|
||||||
|
-> Maybe (E.SqlQuery ())
|
||||||
|
parseSearchQuery toExpr =
|
||||||
|
fmap where_ . either (const Nothing) Just . P.parseOnly andE
|
||||||
|
where
|
||||||
|
andE = foldl1 (&&.) <$> P.many1 (P.skipSpace *> orE <|> tokenTermE)
|
||||||
|
orE = foldl1 (||.) <$> tokenTermE `P.sepBy1` P.char '|'
|
||||||
|
tokenTermE = negE termE <|> termE
|
||||||
|
where
|
||||||
|
negE p = not_ <$> (P.char '-' *> p)
|
||||||
|
termE = toExpr <$> (fieldTerm <|> quotedTerm <|> simpleTerm)
|
||||||
|
fieldTerm = concat <$> sequence [simpleTerm, P.string ":", quotedTerm <|> simpleTerm]
|
||||||
|
quotedTerm = PC.between (P.char '"') (P.char '"') (P.takeWhile1 (/= '"'))
|
||||||
|
simpleTerm = P.takeWhile1 (\c -> not (isSpace c) && c /= ':' && c /= '|')
|
||||||
|
|
||||||
|
parseTimeText :: (TI.ParseTime t, Monad m, Alternative m) => Text -> m t
|
||||||
|
parseTimeText t =
|
||||||
|
asum $
|
||||||
|
flip (parseTimeM True defaultTimeLocale) (unpack t) <$>
|
||||||
|
[ "%-m/%-d/%Y" , "%-m/%-d/%Y%z" , "%-m/%-d/%Y%Z" -- 12/31/2018
|
||||||
|
, "%Y-%-m-%-d" , "%Y-%-m-%-d%z" , "%Y-%-m-%-d%Z" -- 2018-12-31
|
||||||
|
, "%Y-%-m-%-dT%T" , "%Y-%-m-%-dT%T%z" , "%Y-%-m-%-dT%T%Z" -- 2018-12-31T06:40:53
|
||||||
|
, "%s" -- 1535932800
|
||||||
|
]
|
||||||
|
|
||||||
|
tagsQuery :: [Entity Bookmark] -> DB [Entity BookmarkTag]
|
||||||
|
tagsQuery bmarks =
|
||||||
|
select $
|
||||||
|
from $ \t -> do
|
||||||
|
where_ (t ^. BookmarkTagBookmarkId `in_` valList (fmap entityKey bmarks))
|
||||||
|
orderBy [asc (t ^. BookmarkTagSeq)]
|
||||||
|
pure t
|
||||||
|
|
||||||
|
withTags :: Key Bookmark -> DB [Entity BookmarkTag]
|
||||||
|
withTags key = selectList [BookmarkTagBookmarkId ==. key] [Asc BookmarkTagSeq]
|
||||||
|
|
||||||
|
-- Note List Query
|
||||||
|
|
||||||
|
|
||||||
|
getNote :: Key User -> NtSlug -> DB (Maybe (Entity Note))
|
||||||
|
getNote userKey slug =
|
||||||
|
selectFirst [NoteUserId ==. userKey, NoteSlug ==. slug] []
|
||||||
|
|
||||||
|
getNoteList :: Key User -> Maybe Text -> Limit -> Page -> DB (Int, [Entity Note])
|
||||||
|
getNoteList key mquery limit' page =
|
||||||
|
(,) -- total count
|
||||||
|
<$> fmap (sum . fmap E.unValue)
|
||||||
|
(select $
|
||||||
|
from $ \b -> do
|
||||||
|
_whereClause b
|
||||||
|
pure $ E.countRows)
|
||||||
|
<*> (select $
|
||||||
|
from $ \b -> do
|
||||||
|
_whereClause b
|
||||||
|
orderBy [desc (b ^. NoteCreated)]
|
||||||
|
limit limit'
|
||||||
|
offset ((page - 1) * limit')
|
||||||
|
pure b)
|
||||||
|
where
|
||||||
|
_whereClause b = do
|
||||||
|
where_ $ (b ^. NoteUserId E.==. val key)
|
||||||
|
-- search
|
||||||
|
sequenceA_ (parseSearchQuery (toLikeExpr b) =<< mquery)
|
||||||
|
|
||||||
|
toLikeExpr :: E.SqlExpr (Entity Note) -> Text -> E.SqlExpr (E.Value Bool)
|
||||||
|
toLikeExpr b term = fromRight p_allFields (P.parseOnly p_onefield term)
|
||||||
|
where
|
||||||
|
wild s = (E.%) ++. val s ++. (E.%)
|
||||||
|
toLikeN field s = b ^. field `E.like` wild s
|
||||||
|
p_allFields = toLikeN NoteTitle term ||. toLikeN NoteText term
|
||||||
|
p_onefield = p_title <|> p_text <|> p_after <|> p_before
|
||||||
|
where
|
||||||
|
p_title = "title:" *> fmap (toLikeN NoteTitle) P.takeText
|
||||||
|
p_text = "description:" *> fmap (toLikeN NoteText) P.takeText
|
||||||
|
p_after = "after:" *> fmap ((b ^. NoteCreated E.>=.) . val) (parseTimeText =<< P.takeText)
|
||||||
|
p_before = "before:" *> fmap ((b ^. NoteCreated E.<=.) . val) (parseTimeText =<< P.takeText)
|
||||||
|
|
||||||
|
-- Bookmark Files
|
||||||
|
|
||||||
|
bookmarkEntityToTags :: Entity Bookmark -> [Tag] -> [BookmarkTag]
|
||||||
|
bookmarkEntityToTags (Entity {entityKey = bookmarkId
|
||||||
|
,entityVal = Bookmark {..}}) tags =
|
||||||
|
fmap
|
||||||
|
(\(i, tag) -> BookmarkTag bookmarkUserId tag bookmarkId i)
|
||||||
|
(zip [1 ..] tags)
|
||||||
|
|
||||||
|
|
||||||
|
fileBookmarkToBookmark :: UserId -> FileBookmark -> IO Bookmark
|
||||||
|
fileBookmarkToBookmark user (FileBookmark {..}) = do
|
||||||
|
slug <- mkBmSlug
|
||||||
|
pure $
|
||||||
|
Bookmark
|
||||||
|
user
|
||||||
|
slug
|
||||||
|
fileBookmarkHref
|
||||||
|
fileBookmarkDescription
|
||||||
|
fileBookmarkExtended
|
||||||
|
fileBookmarkTime
|
||||||
|
fileBookmarkShared
|
||||||
|
fileBookmarkToRead
|
||||||
|
False
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
|
||||||
|
insertFileBookmarks :: Key User -> FilePath -> DB ()
|
||||||
|
insertFileBookmarks userId bookmarkFile = do
|
||||||
|
mfmarks <- liftIO $ readFileBookmarks bookmarkFile
|
||||||
|
case mfmarks of
|
||||||
|
Left e -> print e
|
||||||
|
Right fmarks -> do
|
||||||
|
bookmarks <- liftIO $ mapM (fileBookmarkToBookmark userId) fmarks
|
||||||
|
mbookmarkIds <- mapM insertUnique bookmarks
|
||||||
|
|
||||||
|
let bookmarkTags =
|
||||||
|
concatMap (uncurry bookmarkEntityToTags) $
|
||||||
|
catMaybes $
|
||||||
|
zipWith3 (\mk v p -> map (\k -> (Entity k v, fileBookmarkTags p)) mk)
|
||||||
|
mbookmarkIds
|
||||||
|
bookmarks
|
||||||
|
fmarks
|
||||||
|
void $ mapM insertUnique bookmarkTags
|
||||||
|
where
|
||||||
|
readFileBookmarks :: MonadIO m => FilePath -> m (Either String [FileBookmark])
|
||||||
|
readFileBookmarks fpath = pure . A.eitherDecode' . fromStrict =<< readFile fpath
|
||||||
|
|
||||||
|
type Tag = Text
|
||||||
|
|
||||||
|
-- Notes
|
||||||
|
|
||||||
|
fileNoteToNote :: UserId -> FileNote -> IO Note
|
||||||
|
fileNoteToNote user (FileNote {..} ) = do
|
||||||
|
slug <- mkNtSlug
|
||||||
|
pure $
|
||||||
|
Note
|
||||||
|
user
|
||||||
|
slug
|
||||||
|
fileNoteLength
|
||||||
|
fileNoteTitle
|
||||||
|
fileNoteText
|
||||||
|
False
|
||||||
|
fileNoteCreatedAt
|
||||||
|
fileNoteUpdatedAt
|
||||||
|
|
||||||
|
insertDirFileNotes :: Key User -> FilePath -> DB ()
|
||||||
|
insertDirFileNotes userId noteDirectory = do
|
||||||
|
mfnotes <- liftIO $ readFileNotes noteDirectory
|
||||||
|
case mfnotes of
|
||||||
|
Left e -> print e
|
||||||
|
Right fnotes -> do
|
||||||
|
notes <- liftIO $ mapM (fileNoteToNote userId) fnotes
|
||||||
|
void $ mapM insertUnique notes
|
||||||
|
where
|
||||||
|
readFileNotes :: MonadIO m => FilePath -> m (Either String [FileNote])
|
||||||
|
readFileNotes fdir = do
|
||||||
|
files <- liftIO (listDirectory fdir)
|
||||||
|
noteBSS <- mapM (readFile . (fdir </>)) files
|
||||||
|
pure (mapM (A.eitherDecode' . fromStrict) noteBSS)
|
||||||
|
|
||||||
|
-- AccountSettingsForm
|
||||||
|
data AccountSettingsForm = AccountSettingsForm
|
||||||
|
{ _privateDefault :: Bool
|
||||||
|
, _archiveDefault :: Bool
|
||||||
|
, _privacyLock :: Bool
|
||||||
|
} deriving (Show, Eq, Read, Generic)
|
||||||
|
|
||||||
|
instance FromJSON AccountSettingsForm where parseJSON = A.genericParseJSON gDefaultFormOptions
|
||||||
|
instance ToJSON AccountSettingsForm where toJSON = A.genericToJSON gDefaultFormOptions
|
||||||
|
|
||||||
|
toAccountSettingsForm :: User -> AccountSettingsForm
|
||||||
|
toAccountSettingsForm (User {..}) =
|
||||||
|
AccountSettingsForm
|
||||||
|
{ _privateDefault = userPrivateDefault
|
||||||
|
, _archiveDefault = userArchiveDefault
|
||||||
|
, _privacyLock = userPrivacyLock
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserFromAccountSettingsForm :: Key User -> AccountSettingsForm -> DB ()
|
||||||
|
updateUserFromAccountSettingsForm userId (AccountSettingsForm {..}) = do
|
||||||
|
CP.update userId
|
||||||
|
[ UserPrivateDefault CP.=. _privateDefault
|
||||||
|
, UserArchiveDefault CP.=. _archiveDefault
|
||||||
|
, UserPrivacyLock CP.=. _privacyLock
|
||||||
|
]
|
||||||
|
|
||||||
|
-- BookmarkForm
|
||||||
|
|
||||||
|
data BookmarkForm = BookmarkForm
|
||||||
|
{ _url :: Text
|
||||||
|
, _title :: Maybe Text
|
||||||
|
, _description :: Maybe Textarea
|
||||||
|
, _tags :: Maybe Text
|
||||||
|
, _private :: Maybe Bool
|
||||||
|
, _toread :: Maybe Bool
|
||||||
|
, _bid :: Maybe Int64
|
||||||
|
, _slug :: Maybe BmSlug
|
||||||
|
, _selected :: Maybe Bool
|
||||||
|
, _time :: Maybe UTCTimeStr
|
||||||
|
, _archiveUrl :: Maybe Text
|
||||||
|
} deriving (Show, Eq, Read, Generic)
|
||||||
|
|
||||||
|
instance FromJSON BookmarkForm where parseJSON = A.genericParseJSON gDefaultFormOptions
|
||||||
|
instance ToJSON BookmarkForm where toJSON = A.genericToJSON gDefaultFormOptions
|
||||||
|
|
||||||
|
gDefaultFormOptions :: A.Options
|
||||||
|
gDefaultFormOptions = A.defaultOptions { A.fieldLabelModifier = drop 1 }
|
||||||
|
|
||||||
|
toBookmarkFormList :: [Entity Bookmark] -> [Entity BookmarkTag] -> [BookmarkForm]
|
||||||
|
toBookmarkFormList bs as = do
|
||||||
|
b <- bs
|
||||||
|
let bid = E.entityKey b
|
||||||
|
let btags = filter ((==) bid . bookmarkTagBookmarkId . E.entityVal) as
|
||||||
|
pure $ _toBookmarkForm (b, btags)
|
||||||
|
|
||||||
|
_toBookmarkForm :: (Entity Bookmark, [Entity BookmarkTag]) -> BookmarkForm
|
||||||
|
_toBookmarkForm (Entity bid Bookmark {..}, tags) =
|
||||||
|
BookmarkForm
|
||||||
|
{ _url = bookmarkHref
|
||||||
|
, _title = Just bookmarkDescription
|
||||||
|
, _description = Just $ Textarea $ bookmarkExtended
|
||||||
|
, _tags = Just $ unwords $ fmap (bookmarkTagTag . entityVal) tags
|
||||||
|
, _private = Just $ not bookmarkShared
|
||||||
|
, _toread = Just $ bookmarkToRead
|
||||||
|
, _bid = Just $ unBookmarkKey $ bid
|
||||||
|
, _slug = Just $ bookmarkSlug
|
||||||
|
, _selected = Just $ bookmarkSelected
|
||||||
|
, _time = Just $ UTCTimeStr $ bookmarkTime
|
||||||
|
, _archiveUrl = bookmarkArchiveHref
|
||||||
|
}
|
||||||
|
|
||||||
|
_toBookmark :: UserId -> BookmarkForm -> IO Bookmark
|
||||||
|
_toBookmark userId BookmarkForm {..} = do
|
||||||
|
time <- liftIO getCurrentTime
|
||||||
|
slug <- maybe mkBmSlug pure _slug
|
||||||
|
pure $
|
||||||
|
Bookmark
|
||||||
|
userId
|
||||||
|
slug
|
||||||
|
_url
|
||||||
|
(fromMaybe "" _title)
|
||||||
|
(maybe "" unTextarea _description)
|
||||||
|
(fromMaybe time (fmap unUTCTimeStr _time))
|
||||||
|
(maybe True not _private)
|
||||||
|
(fromMaybe False _toread)
|
||||||
|
(fromMaybe False _selected)
|
||||||
|
_archiveUrl
|
||||||
|
|
||||||
|
fetchBookmarkByUrl :: Key User -> Maybe Text -> DB (Maybe (Entity Bookmark, [Entity BookmarkTag]))
|
||||||
|
fetchBookmarkByUrl userId murl = runMaybeT $ do
|
||||||
|
bmark <- MaybeT . getBy . UniqueUserHref userId =<< (MaybeT $ pure murl)
|
||||||
|
btags <- lift $ withTags (entityKey bmark)
|
||||||
|
pure (bmark, btags)
|
||||||
|
|
||||||
|
data UpsertResult = Created | Updated
|
||||||
|
|
||||||
|
upsertBookmark:: Maybe (Key Bookmark) -> Bookmark -> [Text] -> DB (UpsertResult, Key Bookmark)
|
||||||
|
upsertBookmark mbid bm tags = do
|
||||||
|
res <- case mbid of
|
||||||
|
Just bid -> do
|
||||||
|
get bid >>= \case
|
||||||
|
Just prev_bm -> replaceBookmark bid prev_bm
|
||||||
|
_ -> fail "not found"
|
||||||
|
Nothing -> do
|
||||||
|
getBy (UniqueUserHref (bookmarkUserId bm) (bookmarkHref bm)) >>= \case
|
||||||
|
Just (Entity bid prev_bm) -> replaceBookmark bid prev_bm
|
||||||
|
_ -> (Created,) <$> insert bm
|
||||||
|
insertTags (bookmarkUserId bm) (snd res)
|
||||||
|
pure res
|
||||||
|
where
|
||||||
|
prepareReplace prev_bm = do
|
||||||
|
if (bookmarkHref bm /= bookmarkHref prev_bm)
|
||||||
|
then bm { bookmarkArchiveHref = Nothing }
|
||||||
|
else bm { bookmarkArchiveHref = bookmarkArchiveHref prev_bm }
|
||||||
|
replaceBookmark bid prev_bm = do
|
||||||
|
replace bid (prepareReplace prev_bm)
|
||||||
|
deleteTags bid
|
||||||
|
pure (Updated, bid)
|
||||||
|
deleteTags bid =
|
||||||
|
deleteWhere [BookmarkTagBookmarkId ==. bid]
|
||||||
|
insertTags userId bid' =
|
||||||
|
for_ (zip [1 ..] tags) $
|
||||||
|
\(i, tag) -> void $ insert $ BookmarkTag userId tag bid' i
|
||||||
|
|
||||||
|
updateBookmarkArchiveUrl :: Key User -> Key Bookmark -> Maybe Text -> DB ()
|
||||||
|
updateBookmarkArchiveUrl userId bid marchiveUrl = do
|
||||||
|
updateWhere
|
||||||
|
[BookmarkUserId ==. userId, BookmarkId ==. bid]
|
||||||
|
[BookmarkArchiveHref CP.=. marchiveUrl]
|
||||||
|
|
||||||
|
upsertNote:: Maybe (Key Note) -> Note -> DB (UpsertResult, Key Note)
|
||||||
|
upsertNote mnid bmark@Note{..} = do
|
||||||
|
case mnid of
|
||||||
|
Just nid -> do
|
||||||
|
get nid >>= \case
|
||||||
|
Just _ -> do
|
||||||
|
replace nid bmark
|
||||||
|
pure (Updated, nid)
|
||||||
|
_ -> fail "not found"
|
||||||
|
Nothing -> do
|
||||||
|
(Created,) <$> insert bmark
|
||||||
|
|
||||||
|
-- * FileBookmarks
|
||||||
|
|
||||||
|
data FileBookmark = FileBookmark
|
||||||
|
{ fileBookmarkHref :: !Text
|
||||||
|
, fileBookmarkDescription :: !Text
|
||||||
|
, fileBookmarkExtended :: !Text
|
||||||
|
, fileBookmarkTime :: !UTCTime
|
||||||
|
, fileBookmarkShared :: !Bool
|
||||||
|
, fileBookmarkToRead :: !Bool
|
||||||
|
, fileBookmarkTags :: [Tag]
|
||||||
|
} deriving (Show, Eq, Typeable, Ord)
|
||||||
|
|
||||||
|
instance FromJSON FileBookmark where
|
||||||
|
parseJSON (Object o) =
|
||||||
|
FileBookmark <$> o .: "href" <*> o .: "description" <*> o .: "extended" <*>
|
||||||
|
o .: "time" <*>
|
||||||
|
(boolFromYesNo <$> o .: "shared") <*>
|
||||||
|
(boolFromYesNo <$> o .: "toread") <*>
|
||||||
|
(words <$> o .: "tags")
|
||||||
|
parseJSON _ = fail "bad parse"
|
||||||
|
|
||||||
|
boolFromYesNo :: Text -> Bool
|
||||||
|
boolFromYesNo "yes" = True
|
||||||
|
boolFromYesNo _ = False
|
||||||
|
|
||||||
|
-- * FileNotes
|
||||||
|
|
||||||
|
data FileNote = FileNote
|
||||||
|
{ fileNoteId :: !Text
|
||||||
|
, fileNoteTitle :: !Text
|
||||||
|
, fileNoteText :: !Text
|
||||||
|
, fileNoteLength :: !Int
|
||||||
|
, fileNoteCreatedAt :: !UTCTime
|
||||||
|
, fileNoteUpdatedAt :: !UTCTime
|
||||||
|
} deriving (Show, Eq, Typeable, Ord)
|
||||||
|
|
||||||
|
instance FromJSON FileNote where
|
||||||
|
parseJSON (Object o) =
|
||||||
|
FileNote <$> o .: "id" <*> o .: "title" <*> o .: "text" <*>
|
||||||
|
o .: "length" <*>
|
||||||
|
(readFileNoteTime =<< o .: "created_at") <*>
|
||||||
|
(readFileNoteTime =<< o .: "updated_at")
|
||||||
|
parseJSON _ = fail "bad parse"
|
||||||
|
|
||||||
|
readFileNoteTime
|
||||||
|
:: Monad m
|
||||||
|
=> String -> m UTCTime
|
||||||
|
readFileNoteTime = parseTimeM True defaultTimeLocale "%F %T"
|
60
src/ModelCustom.hs
Normal file
60
src/ModelCustom.hs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
|
||||||
|
module ModelCustom where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Crypto.BCrypt as Import hiding (hashPassword)
|
||||||
|
import Database.Persist.Sql
|
||||||
|
import Safe (fromJustNote)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as TE
|
||||||
|
import qualified Data.Aeson as A
|
||||||
|
import System.Entropy (getEntropy)
|
||||||
|
import qualified Data.ByteString.Builder as BB
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
|
||||||
|
mkSlug :: Int -> IO T.Text
|
||||||
|
mkSlug size =
|
||||||
|
TE.decodeUtf8 . LBS.toStrict . BB.toLazyByteString . BB.byteStringHex <$>
|
||||||
|
getEntropy size
|
||||||
|
|
||||||
|
-- * Bookmark Slug
|
||||||
|
|
||||||
|
newtype BmSlug = BmSlug
|
||||||
|
{ unBmSlug :: T.Text
|
||||||
|
} deriving (Eq, PersistField, PersistFieldSql, Show, Read, Ord, A.FromJSON, A.ToJSON)
|
||||||
|
|
||||||
|
mkBmSlug :: IO BmSlug
|
||||||
|
mkBmSlug = BmSlug <$> mkSlug 6
|
||||||
|
|
||||||
|
-- * Note Slug
|
||||||
|
|
||||||
|
newtype NtSlug = NtSlug
|
||||||
|
{ unNtSlug :: T.Text
|
||||||
|
} deriving (Eq, PersistField, PersistFieldSql, Show, Read, Ord, A.FromJSON, A.ToJSON)
|
||||||
|
|
||||||
|
mkNtSlug :: IO NtSlug
|
||||||
|
mkNtSlug = NtSlug <$> mkSlug 10
|
||||||
|
|
||||||
|
-- * Model Crypto
|
||||||
|
|
||||||
|
policy :: HashingPolicy
|
||||||
|
policy =
|
||||||
|
HashingPolicy
|
||||||
|
{ preferredHashCost = 12
|
||||||
|
, preferredHashAlgorithm = "$2a$"
|
||||||
|
}
|
||||||
|
|
||||||
|
newtype BCrypt = BCrypt
|
||||||
|
{ unBCrypt :: T.Text
|
||||||
|
} deriving (Eq, PersistField, PersistFieldSql, Show, Ord, A.FromJSON, A.ToJSON)
|
||||||
|
|
||||||
|
hashPassword :: T.Text -> IO BCrypt
|
||||||
|
hashPassword rawPassword = do
|
||||||
|
mPassword <- hashPasswordUsingPolicy policy (TE.encodeUtf8 rawPassword)
|
||||||
|
return
|
||||||
|
(BCrypt (TE.decodeUtf8 (fromJustNote "Invalid hashing policy" mPassword)))
|
||||||
|
|
||||||
|
validatePasswordHash :: BCrypt -> T.Text -> Bool
|
||||||
|
validatePasswordHash hash' pass = do
|
||||||
|
validatePassword (TE.encodeUtf8 (unBCrypt hash')) (TE.encodeUtf8 pass)
|
55
src/PathPiece.hs
Normal file
55
src/PathPiece.hs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module PathPiece where
|
||||||
|
|
||||||
|
import Data.Text (splitOn)
|
||||||
|
|
||||||
|
import Import.NoFoundation
|
||||||
|
|
||||||
|
-- PathPiece
|
||||||
|
|
||||||
|
instance PathPiece UserNameP where
|
||||||
|
toPathPiece (UserNameP i) = "u:" <> i
|
||||||
|
fromPathPiece s =
|
||||||
|
case splitOn ":" s of
|
||||||
|
["u", ""] -> Nothing
|
||||||
|
["u", uname] -> Just $ UserNameP uname
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
instance PathPiece TagsP where
|
||||||
|
toPathPiece (TagsP tags) = "t:" <> (intercalate "+" tags)
|
||||||
|
fromPathPiece s =
|
||||||
|
case splitOn ":" s of
|
||||||
|
["t", ""] -> Nothing
|
||||||
|
["t", tags] -> Just $ TagsP (splitOn "+" tags)
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
instance PathPiece SharedP where
|
||||||
|
toPathPiece = \case
|
||||||
|
SharedAll -> ""
|
||||||
|
SharedPublic -> "public"
|
||||||
|
SharedPrivate -> "private"
|
||||||
|
fromPathPiece = \case
|
||||||
|
"public" -> Just SharedPublic
|
||||||
|
"private" -> Just SharedPrivate
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
instance PathPiece FilterP where
|
||||||
|
toPathPiece = \case
|
||||||
|
FilterAll -> ""
|
||||||
|
FilterUnread -> "unread"
|
||||||
|
FilterUntagged -> "untagged"
|
||||||
|
FilterStarred -> "starred"
|
||||||
|
FilterSingle slug -> "b:" <> unBmSlug slug
|
||||||
|
fromPathPiece = \case
|
||||||
|
"unread" -> Just FilterUnread
|
||||||
|
"untagged" -> Just FilterUntagged
|
||||||
|
"starred" -> Just FilterStarred
|
||||||
|
s -> case splitOn ":" s of
|
||||||
|
["b", ""] -> Nothing
|
||||||
|
["b", slug] -> Just $ FilterSingle (BmSlug slug)
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
|
||||||
|
deriving instance PathPiece NtSlug
|
||||||
|
deriving instance PathPiece BmSlug
|
15
src/Pretty.hs
Normal file
15
src/Pretty.hs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module Pretty where
|
||||||
|
|
||||||
|
import Text.Show.Pretty (ppShow)
|
||||||
|
import Language.Haskell.HsColour
|
||||||
|
import Language.Haskell.HsColour.Colourise
|
||||||
|
import ClassyPrelude
|
||||||
|
|
||||||
|
cpprint :: (MonadIO m, Show a) => a -> m ()
|
||||||
|
cpprint = putStrLn . pack . hscolour TTY defaultColourPrefs False False "" False . ppShow
|
||||||
|
|
||||||
|
cprint :: (MonadIO m, Show a) => a -> m ()
|
||||||
|
cprint = putStrLn . pack . hscolour TTY defaultColourPrefs False False "" False . show
|
||||||
|
|
||||||
|
pprint :: (MonadIO m, Show a) => a -> m ()
|
||||||
|
pprint = putStrLn . pack . ppShow
|
150
src/Settings.hs
Normal file
150
src/Settings.hs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
-- | Settings are centralized, as much as possible, into this file. This
|
||||||
|
-- includes database connection settings, static file locations, etc.
|
||||||
|
-- In addition, you can configure a number of different aspects of Yesod
|
||||||
|
-- by overriding methods in the Yesod typeclass. That instance is
|
||||||
|
-- declared in the Foundation.hs file.
|
||||||
|
module Settings where
|
||||||
|
|
||||||
|
import ClassyPrelude.Yesod
|
||||||
|
import qualified Control.Exception as Exception
|
||||||
|
import Data.Aeson (Result (..), fromJSON, withObject, (.!=),
|
||||||
|
(.:?))
|
||||||
|
import Data.FileEmbed (embedFile)
|
||||||
|
import Data.Yaml (decodeEither')
|
||||||
|
import Database.Persist.Sqlite (SqliteConf)
|
||||||
|
import Language.Haskell.TH.Syntax (Exp, Name, Q)
|
||||||
|
import Network.Wai.Handler.Warp (HostPreference)
|
||||||
|
import Yesod.Default.Config2 (applyEnvValue, configSettingsYml)
|
||||||
|
import Yesod.Default.Util (WidgetFileSettings, widgetFileNoReload,
|
||||||
|
widgetFileReload)
|
||||||
|
|
||||||
|
-- | Runtime settings to configure this application. These settings can be
|
||||||
|
-- loaded from various sources: defaults, environment variables, config files,
|
||||||
|
-- theoretically even a database.
|
||||||
|
data AppSettings = AppSettings
|
||||||
|
{ appStaticDir :: String
|
||||||
|
-- ^ Directory from which to serve static files.
|
||||||
|
, appDatabaseConf :: SqliteConf
|
||||||
|
-- ^ Configuration settings for accessing the database.
|
||||||
|
, appRoot :: Maybe Text
|
||||||
|
-- ^ Base for all generated URLs. If @Nothing@, determined
|
||||||
|
-- from the request headers.
|
||||||
|
, appHost :: HostPreference
|
||||||
|
-- ^ Host/interface the server should bind to.
|
||||||
|
, appPort :: Int
|
||||||
|
-- ^ Port to listen on
|
||||||
|
, appIpFromHeader :: Bool
|
||||||
|
-- ^ Get the IP address from the header when logging. Useful when sitting
|
||||||
|
-- behind a reverse proxy.
|
||||||
|
, appDetailedRequestLogging :: Bool
|
||||||
|
-- ^ Use detailed request logging system
|
||||||
|
, appShouldLogAll :: Bool
|
||||||
|
-- ^ Should all log messages be displayed?
|
||||||
|
, appReloadTemplates :: Bool
|
||||||
|
-- ^ Use the reload version of templates
|
||||||
|
, appMutableStatic :: Bool
|
||||||
|
-- ^ Assume that files in the static dir may change after compilation
|
||||||
|
, appSkipCombining :: Bool
|
||||||
|
-- ^ Perform no stylesheet/script combining
|
||||||
|
|
||||||
|
-- Example app-specific configuration values.
|
||||||
|
, appCopyright :: Text
|
||||||
|
-- ^ Copyright text to appear in the footer of the page
|
||||||
|
, appAnalytics :: Maybe Text
|
||||||
|
-- ^ Google Analytics code
|
||||||
|
|
||||||
|
, appAuthDummyLogin :: Bool
|
||||||
|
-- ^ Indicate if auth dummy login should be enabled.
|
||||||
|
|
||||||
|
, appEkgHost :: Maybe Text
|
||||||
|
-- ^ Host/interface the ekg server should bind to.
|
||||||
|
, appEkgPort :: Maybe Int
|
||||||
|
-- ^ Port to listen on
|
||||||
|
}
|
||||||
|
|
||||||
|
instance FromJSON AppSettings where
|
||||||
|
parseJSON = withObject "AppSettings" $ \o -> do
|
||||||
|
let defaultDev =
|
||||||
|
#ifdef DEVELOPMENT
|
||||||
|
True
|
||||||
|
#else
|
||||||
|
False
|
||||||
|
#endif
|
||||||
|
appStaticDir <- o .: "static-dir"
|
||||||
|
appDatabaseConf <- o .: "database"
|
||||||
|
appRoot <- o .:? "approot"
|
||||||
|
appHost <- fromString <$> o .: "host"
|
||||||
|
appPort <- o .: "port"
|
||||||
|
appIpFromHeader <- o .: "ip-from-header"
|
||||||
|
|
||||||
|
dev <- o .:? "development" .!= defaultDev
|
||||||
|
|
||||||
|
appDetailedRequestLogging <- o .:? "detailed-logging" .!= dev
|
||||||
|
appShouldLogAll <- o .:? "should-log-all" .!= dev
|
||||||
|
appReloadTemplates <- o .:? "reload-templates" .!= dev
|
||||||
|
appMutableStatic <- o .:? "mutable-static" .!= dev
|
||||||
|
appSkipCombining <- o .:? "skip-combining" .!= dev
|
||||||
|
|
||||||
|
appCopyright <- o .: "copyright"
|
||||||
|
appAnalytics <- o .:? "analytics"
|
||||||
|
|
||||||
|
appAuthDummyLogin <- o .:? "auth-dummy-login" .!= dev
|
||||||
|
|
||||||
|
appEkgHost <- o .:? "ekg-host"
|
||||||
|
appEkgPort <- o .:? "ekg-port"
|
||||||
|
|
||||||
|
return AppSettings {..}
|
||||||
|
|
||||||
|
-- | Settings for 'widgetFile', such as which template languages to support and
|
||||||
|
-- default Hamlet settings.
|
||||||
|
--
|
||||||
|
-- For more information on modifying behavior, see:
|
||||||
|
--
|
||||||
|
-- https://github.com/yesodweb/yesod/wiki/Overriding-widgetFile
|
||||||
|
widgetFileSettings :: WidgetFileSettings
|
||||||
|
widgetFileSettings = def
|
||||||
|
|
||||||
|
-- | How static files should be combined.
|
||||||
|
combineSettings :: CombineSettings
|
||||||
|
combineSettings = def
|
||||||
|
|
||||||
|
-- The rest of this file contains settings which rarely need changing by a
|
||||||
|
-- user.
|
||||||
|
|
||||||
|
widgetFile :: String -> Q Exp
|
||||||
|
widgetFile = (if appReloadTemplates compileTimeAppSettings
|
||||||
|
then widgetFileReload
|
||||||
|
else widgetFileNoReload)
|
||||||
|
widgetFileSettings
|
||||||
|
|
||||||
|
-- | Raw bytes at compile time of @config/settings.yml@
|
||||||
|
configSettingsYmlBS :: ByteString
|
||||||
|
configSettingsYmlBS = $(embedFile configSettingsYml)
|
||||||
|
|
||||||
|
-- | @config/settings.yml@, parsed to a @Value@.
|
||||||
|
configSettingsYmlValue :: Value
|
||||||
|
configSettingsYmlValue = either Exception.throw id
|
||||||
|
$ decodeEither' configSettingsYmlBS
|
||||||
|
|
||||||
|
-- | A version of @AppSettings@ parsed at compile time from @config/settings.yml@.
|
||||||
|
compileTimeAppSettings :: AppSettings
|
||||||
|
compileTimeAppSettings =
|
||||||
|
case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of
|
||||||
|
Error e -> error e
|
||||||
|
Success settings -> settings
|
||||||
|
|
||||||
|
-- The following two functions can be used to combine multiple CSS or JS files
|
||||||
|
-- at compile time to decrease the number of http requests.
|
||||||
|
-- Sample usage (inside a Widget):
|
||||||
|
--
|
||||||
|
-- > $(combineStylesheets 'StaticR [style1_css, style2_css])
|
||||||
|
|
||||||
|
combineStylesheets :: Name -> [Route Static] -> Q Exp
|
||||||
|
combineStylesheets = combineStylesheets'
|
||||||
|
(appSkipCombining compileTimeAppSettings)
|
||||||
|
combineSettings
|
||||||
|
|
||||||
|
combineScripts :: Name -> [Route Static] -> Q Exp
|
||||||
|
combineScripts = combineScripts'
|
||||||
|
(appSkipCombining compileTimeAppSettings)
|
||||||
|
combineSettings
|
18
src/Settings/StaticFiles.hs
Normal file
18
src/Settings/StaticFiles.hs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module Settings.StaticFiles where
|
||||||
|
|
||||||
|
import Settings (appStaticDir, compileTimeAppSettings)
|
||||||
|
import Yesod.Static (staticFiles)
|
||||||
|
|
||||||
|
-- This generates easy references to files in the static directory at compile time,
|
||||||
|
-- giving you compile-time verification that referenced files exist.
|
||||||
|
-- Warning: any files added to your static directory during run-time can't be
|
||||||
|
-- accessed this way. You'll have to use their FilePath or URL to access them.
|
||||||
|
--
|
||||||
|
-- For example, to refer to @static/js/script.js@ via an identifier, you'd use:
|
||||||
|
--
|
||||||
|
-- js_script_js
|
||||||
|
--
|
||||||
|
-- If the identifier is not available, you may use:
|
||||||
|
--
|
||||||
|
-- StaticFile ["js", "script.js"] []
|
||||||
|
staticFiles (appStaticDir compileTimeAppSettings)
|
13
src/Types.hs
Normal file
13
src/Types.hs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module Types where
|
||||||
|
|
||||||
|
import ClassyPrelude.Yesod
|
||||||
|
|
||||||
|
type DBM m a = MonadUnliftIO m => SqlPersistT m a
|
||||||
|
|
||||||
|
type DB a = forall m. DBM m a
|
||||||
|
|
||||||
|
type DBVal val =
|
||||||
|
( PersistEntity val
|
||||||
|
, PersistEntityBackend val ~ SqlBackend
|
||||||
|
, PersistStore (PersistEntityBackend val))
|
||||||
|
|
11
stack.yaml
Normal file
11
stack.yaml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
resolver: lts-13.0
|
||||||
|
# allow-newer: true
|
||||||
|
extra-deps:
|
||||||
|
- git: https://github.com/bitemyapp/esqueleto.git
|
||||||
|
commit: 5f98e7b25334ec120125ca84ef647d5c4575a010
|
||||||
|
- ekg-0.4.0.15
|
||||||
|
- ekg-json-0.1.0.6
|
||||||
|
- monad-metrics-0.2.1.2
|
||||||
|
- wai-middleware-metrics-0.2.4
|
||||||
|
packages:
|
||||||
|
- '.'
|
161
static/css/main.css
Normal file
161
static/css/main.css
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
html {
|
||||||
|
height: 102%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 102%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background:none;
|
||||||
|
border:none;
|
||||||
|
padding:0;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important
|
||||||
|
}
|
||||||
|
input::placeholder {
|
||||||
|
color: lightgray
|
||||||
|
}
|
||||||
|
.queryInput {
|
||||||
|
width: 128px;
|
||||||
|
padding: 0 22px 0 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: gray;
|
||||||
|
height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
transition: width .1s ease-in-out
|
||||||
|
}
|
||||||
|
.queryInput.search-inactive {}
|
||||||
|
.queryInput:focus {
|
||||||
|
width: 175px;
|
||||||
|
}
|
||||||
|
.submitting .queryInput,
|
||||||
|
.queryInput.search-active {
|
||||||
|
border-color: #990;
|
||||||
|
border-width: 2px;
|
||||||
|
background-color: #FF9;
|
||||||
|
width: 175px;
|
||||||
|
}
|
||||||
|
.queryIcon {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top:1px;
|
||||||
|
cursor:pointer;
|
||||||
|
width:20px;
|
||||||
|
height: 20px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.close-x-wrap {
|
||||||
|
float: left;
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
top: 2px;
|
||||||
|
position: relative;
|
||||||
|
right: 2px;
|
||||||
|
}
|
||||||
|
.close-x {
|
||||||
|
stroke: gray;
|
||||||
|
fill: transparent;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
.query-info-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: -18px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
.star {
|
||||||
|
margin-left:-20px;
|
||||||
|
font-size:1.2em;
|
||||||
|
position:relative;
|
||||||
|
top:-2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star button {
|
||||||
|
transition: color .1s;
|
||||||
|
}
|
||||||
|
.star.selected button {
|
||||||
|
color:#22a;
|
||||||
|
}
|
||||||
|
.edit_links button {
|
||||||
|
transition: color .1s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
color:#a51;
|
||||||
|
line-height:190%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.private { background:#ddd;border:1px solid #d1d1d1; }
|
||||||
|
.unread { color:#b41 }
|
||||||
|
.mark_read {color: #a81;}
|
||||||
|
.flash { color:green;background:#efe }
|
||||||
|
|
||||||
|
.top_menu {
|
||||||
|
margin-top:6px;
|
||||||
|
}
|
||||||
|
.top_menu a {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
.bookmarklet {
|
||||||
|
padding:1px 2px 0px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
background:#ced;
|
||||||
|
border:1px solid #acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit_bookmark_form {color:#888;}
|
||||||
|
.edit_bookmark_form input {border:1px solid #ddd;}
|
||||||
|
.edit_bookmark_form textarea {border:1px solid #ddd;}
|
||||||
|
|
||||||
|
.nav-active {
|
||||||
|
background:#ff8;
|
||||||
|
color:blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* mobile device */
|
||||||
|
@media only screen and (max-width : 750px) {
|
||||||
|
body {
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
}
|
||||||
|
.display {
|
||||||
|
float: none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width : 500px) {
|
||||||
|
.filters {
|
||||||
|
clear: both;
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdim {
|
||||||
|
opacity: .8;
|
||||||
|
transition: all .15s ease-in;
|
||||||
|
}
|
||||||
|
.rdim:hover,
|
||||||
|
.rdim:focus {
|
||||||
|
opacity: 1;
|
||||||
|
transition: all .15s ease-in;
|
||||||
|
}
|
35
static/css/popup.css
Normal file
35
static/css/popup.css
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
[hidden] {
|
||||||
|
display: none !important
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background:none;
|
||||||
|
border:none;
|
||||||
|
padding:0;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
button:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.alert {
|
||||||
|
background:#ced;
|
||||||
|
border:1px solid #acc;
|
||||||
|
}
|
||||||
|
form label {
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: table-cell;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
li { list-style-type: none; margin: 0; padding: 0; display: block;}
|
||||||
|
|
||||||
|
.when { color:#999}
|
||||||
|
.unread { color:#b41 }
|
||||||
|
a.unread { color:#b41 }
|
||||||
|
a.bookmark_title { font-size:120%;}
|
||||||
|
|
||||||
|
label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
3
static/css/tachyons.min.css
vendored
Normal file
3
static/css/tachyons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/css/tachyons.min.css.gz
Normal file
BIN
static/css/tachyons.min.css.gz
Normal file
Binary file not shown.
BIN
static/fonts/glyphicons-halflings-regular.eot
Normal file
BIN
static/fonts/glyphicons-halflings-regular.eot
Normal file
Binary file not shown.
229
static/fonts/glyphicons-halflings-regular.svg
Normal file
229
static/fonts/glyphicons-halflings-regular.svg
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<metadata></metadata>
|
||||||
|
<defs>
|
||||||
|
<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
|
||||||
|
<font-face units-per-em="1200" ascent="960" descent="-240" />
|
||||||
|
<missing-glyph horiz-adv-x="500" />
|
||||||
|
<glyph />
|
||||||
|
<glyph />
|
||||||
|
<glyph unicode="
" />
|
||||||
|
<glyph unicode=" " />
|
||||||
|
<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
|
||||||
|
<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
|
||||||
|
<glyph unicode=" " />
|
||||||
|
<glyph unicode=" " horiz-adv-x="652" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="1304" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="652" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="1304" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="434" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="326" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="217" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="217" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="163" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="260" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="72" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="260" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="326" />
|
||||||
|
<glyph unicode="€" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" />
|
||||||
|
<glyph unicode="−" d="M200 400h900v300h-900v-300z" />
|
||||||
|
<glyph unicode="◼" horiz-adv-x="500" d="M0 0z" />
|
||||||
|
<glyph unicode="☁" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" />
|
||||||
|
<glyph unicode="✉" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
|
||||||
|
<glyph unicode="✏" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" />
|
||||||
|
<glyph unicode="" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
|
||||||
|
<glyph unicode="" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q18 -55 86 -75.5t147 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" />
|
||||||
|
<glyph unicode="" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
|
||||||
|
<glyph unicode="" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" />
|
||||||
|
<glyph unicode="" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" />
|
||||||
|
<glyph unicode="" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" />
|
||||||
|
<glyph unicode="" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" />
|
||||||
|
<glyph unicode="" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" />
|
||||||
|
<glyph unicode="" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||||
|
<glyph unicode="" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||||
|
<glyph unicode="" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||||
|
<glyph unicode="" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
|
||||||
|
<glyph unicode="" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
|
||||||
|
<glyph unicode="" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" />
|
||||||
|
<glyph unicode="" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" />
|
||||||
|
<glyph unicode="" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" />
|
||||||
|
<glyph unicode="" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
|
||||||
|
<glyph unicode="" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" />
|
||||||
|
<glyph unicode="" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
|
||||||
|
<glyph unicode="" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
|
||||||
|
<glyph unicode="" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" />
|
||||||
|
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" />
|
||||||
|
<glyph unicode="" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
|
||||||
|
<glyph unicode="" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
|
||||||
|
<glyph unicode="" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" />
|
||||||
|
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" />
|
||||||
|
<glyph unicode="" d="M0 25v475l200 700h800l199 -700l1 -475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" />
|
||||||
|
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" />
|
||||||
|
<glyph unicode="" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" />
|
||||||
|
<glyph unicode="" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" />
|
||||||
|
<glyph unicode="" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" />
|
||||||
|
<glyph unicode="" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
|
||||||
|
<glyph unicode="" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" />
|
||||||
|
<glyph unicode="" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" />
|
||||||
|
<glyph unicode="" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" />
|
||||||
|
<glyph unicode="" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
|
||||||
|
<glyph unicode="" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" />
|
||||||
|
<glyph unicode="" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" />
|
||||||
|
<glyph unicode="" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" />
|
||||||
|
<glyph unicode="" d="M0 700l1 475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" />
|
||||||
|
<glyph unicode="" d="M1 700l1 475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" />
|
||||||
|
<glyph unicode="" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
|
||||||
|
<glyph unicode="" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
|
||||||
|
<glyph unicode="" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" />
|
||||||
|
<glyph unicode="" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" />
|
||||||
|
<glyph unicode="" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" />
|
||||||
|
<glyph unicode="" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v71l471 -1q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" />
|
||||||
|
<glyph unicode="" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" />
|
||||||
|
<glyph unicode="" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " />
|
||||||
|
<glyph unicode="" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" />
|
||||||
|
<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
|
||||||
|
<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
|
||||||
|
<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" />
|
||||||
|
<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
|
||||||
|
<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
|
||||||
|
<glyph unicode="" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" />
|
||||||
|
<glyph unicode="" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" />
|
||||||
|
<glyph unicode="" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" />
|
||||||
|
<glyph unicode="" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " />
|
||||||
|
<glyph unicode="" d="M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z" />
|
||||||
|
<glyph unicode="" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z" />
|
||||||
|
<glyph unicode="" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 139t-64 210zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" />
|
||||||
|
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z" />
|
||||||
|
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" />
|
||||||
|
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q61 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l567 567l-137 137l-430 -431l-146 147z" />
|
||||||
|
<glyph unicode="" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" />
|
||||||
|
<glyph unicode="" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||||
|
<glyph unicode="" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||||
|
<glyph unicode="" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" />
|
||||||
|
<glyph unicode="" d="M200 0l900 550l-900 550v-1100z" />
|
||||||
|
<glyph unicode="" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
|
||||||
|
<glyph unicode="" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
|
||||||
|
<glyph unicode="" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" />
|
||||||
|
<glyph unicode="" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" />
|
||||||
|
<glyph unicode="" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" />
|
||||||
|
<glyph unicode="" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" />
|
||||||
|
<glyph unicode="" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" />
|
||||||
|
<glyph unicode="" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" />
|
||||||
|
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" />
|
||||||
|
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h600v200h-600v-200z" />
|
||||||
|
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141 z" />
|
||||||
|
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" />
|
||||||
|
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM364 700h143q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5 q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-50 0 -90.5 -12t-75 -38.5t-53.5 -74.5t-19 -114zM500 300h200v100h-200 v-100z" />
|
||||||
|
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z" />
|
||||||
|
<glyph unicode="" d="M0 500v200h195q31 125 98.5 199.5t206.5 100.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200v-206 q149 48 201 206h-201v200h200q-25 74 -75.5 127t-124.5 77v-204h-200v203q-75 -23 -130 -77t-79 -126h209v-200h-210z" />
|
||||||
|
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z" />
|
||||||
|
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z" />
|
||||||
|
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z" />
|
||||||
|
<glyph unicode="" d="M0 547l600 453v-300h600v-300h-600v-301z" />
|
||||||
|
<glyph unicode="" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" />
|
||||||
|
<glyph unicode="" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" />
|
||||||
|
<glyph unicode="" d="M104 600h296v600h300v-600h298l-449 -600z" />
|
||||||
|
<glyph unicode="" d="M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z" />
|
||||||
|
<glyph unicode="" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" />
|
||||||
|
<glyph unicode="" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" />
|
||||||
|
<glyph unicode="" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5h-207q-21 0 -33 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" />
|
||||||
|
<glyph unicode="" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111q1 1 1 6.5t-1.5 15t-3.5 17.5l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6 h-111v-100zM100 0h400v400h-400v-400zM200 900q-3 0 14 48t36 96l18 47l213 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" />
|
||||||
|
<glyph unicode="" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" />
|
||||||
|
<glyph unicode="" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" />
|
||||||
|
<glyph unicode="" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" />
|
||||||
|
<glyph unicode="" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" />
|
||||||
|
<glyph unicode="" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 34 -48 36.5t-48 -29.5l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" />
|
||||||
|
<glyph unicode="" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -20 -13 -28.5t-32 0.5l-94 78h-222l-94 -78q-19 -9 -32 -0.5t-13 28.5 v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" />
|
||||||
|
<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" />
|
||||||
|
<glyph unicode="" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" />
|
||||||
|
<glyph unicode="" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" />
|
||||||
|
<glyph unicode="" d="M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" />
|
||||||
|
<glyph unicode="" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" />
|
||||||
|
<glyph unicode="" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" />
|
||||||
|
<glyph unicode="" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" />
|
||||||
|
<glyph unicode="" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" />
|
||||||
|
<glyph unicode="" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" />
|
||||||
|
<glyph unicode="" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" />
|
||||||
|
<glyph unicode="" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
|
||||||
|
<glyph unicode="" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" />
|
||||||
|
<glyph unicode="" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" />
|
||||||
|
<glyph unicode="" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM100 500v250v8v8v7t0.5 7t1.5 5.5t2 5t3 4t4.5 3.5t6 1.5t7.5 0.5h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35 q-55 337 -55 351zM1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" />
|
||||||
|
<glyph unicode="" d="M74 350q0 21 13.5 35.5t33.5 14.5h18l117 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5q-18 -36 -18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-8 -3 -23 -8.5 t-65 -20t-103 -25t-132.5 -19.5t-158.5 -9q-125 0 -245.5 20.5t-178.5 40.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" />
|
||||||
|
<glyph unicode="" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" />
|
||||||
|
<glyph unicode="" d="M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q124 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 213l100 212h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z" />
|
||||||
|
<glyph unicode="" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q124 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z" />
|
||||||
|
<glyph unicode="" d="M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z" />
|
||||||
|
<glyph unicode="" d="M-101 651q0 72 54 110t139 38l302 -1l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 17 -10.5t26.5 -26t16.5 -36.5v-526q0 -13 -86 -93.5t-94 -80.5h-341q-16 0 -29.5 20t-19.5 41l-130 339h-107q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l107 89v502l-343 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM1000 201v600h200v-600h-200z" />
|
||||||
|
<glyph unicode="" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6.5v7.5v6.5v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z" />
|
||||||
|
<glyph unicode="" d="M2 585q-16 -31 6 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85q0 -51 -0.5 -153.5t-0.5 -148.5q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM77 565l236 339h503 l89 -100v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" />
|
||||||
|
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM298 701l2 -201h300l-2 -194l402 294l-402 298v-197h-300z" />
|
||||||
|
<glyph unicode="" d="M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l402 -294l-2 194h300l2 201h-300v197z" />
|
||||||
|
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z" />
|
||||||
|
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z" />
|
||||||
|
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60 q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q104 -3 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5 t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5 q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 39 2 44q31 -13 58 -14.5t39 3.5l11 4q7 36 -16.5 53.5t-64.5 28.5t-56 23q-19 -3 -37 0 q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5zM518 916q3 12 16 30t16 25q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -24 17 -66.5t17 -43.5 q-9 2 -31 5t-36 5t-32 8t-30 14zM692 1003h1h-1z" />
|
||||||
|
<glyph unicode="" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" />
|
||||||
|
<glyph unicode="" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" />
|
||||||
|
<glyph unicode="" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
|
||||||
|
<glyph unicode="" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" />
|
||||||
|
<glyph unicode="" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" />
|
||||||
|
<glyph unicode="" d="M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM514 609q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z" />
|
||||||
|
<glyph unicode="" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -78.5 -16.5t-67.5 -51.5l-389 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23 q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60 l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z" />
|
||||||
|
<glyph unicode="" d="M80 784q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100q-71 70 -104.5 105.5t-77 89.5t-61 99 t-17.5 91zM250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-105 48.5q-74 0 -132 -83l-118 -171l-114 174q-51 80 -123 80q-60 0 -109.5 -49.5t-49.5 -118.5z" />
|
||||||
|
<glyph unicode="" d="M57 353q0 -95 66 -159l141 -142q68 -66 159 -66q93 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-8 9 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141q7 -7 19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -17q47 -49 77 -100l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" />
|
||||||
|
<glyph unicode="" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" />
|
||||||
|
<glyph unicode="" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" />
|
||||||
|
<glyph unicode="" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335q-6 1 -15.5 4t-11.5 3q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5 v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5 zM700 237q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" />
|
||||||
|
<glyph unicode="" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -28 16.5 -69.5t28 -62.5t41.5 -72h241v-100h-197q8 -50 -2.5 -115 t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q33 1 103 -16t103 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221z" />
|
||||||
|
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" />
|
||||||
|
<glyph unicode="" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" />
|
||||||
|
<glyph unicode="" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" />
|
||||||
|
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" />
|
||||||
|
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" />
|
||||||
|
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" />
|
||||||
|
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" />
|
||||||
|
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" />
|
||||||
|
<glyph unicode="" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" />
|
||||||
|
<glyph unicode="" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" />
|
||||||
|
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" />
|
||||||
|
<glyph unicode="" d="M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z" />
|
||||||
|
<glyph unicode="" d="M217 519q8 -19 31 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8h9q14 0 26 15q11 13 274.5 321.5t264.5 308.5q14 19 5 36q-8 17 -31 17l-301 -1q1 4 78 219.5t79 227.5q2 15 -5 27l-9 9h-9q-15 0 -25 -16q-4 -6 -98 -111.5t-228.5 -257t-209.5 -237.5q-16 -19 -6 -41 z" />
|
||||||
|
<glyph unicode="" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z " />
|
||||||
|
<glyph unicode="" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z" />
|
||||||
|
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z" />
|
||||||
|
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z" />
|
||||||
|
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" />
|
||||||
|
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z" />
|
||||||
|
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 400l697 1l3 699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z" />
|
||||||
|
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l249 -237l-1 697zM900 150h100v50h-100v-50z" />
|
||||||
|
<glyph unicode="" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" />
|
||||||
|
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z" />
|
||||||
|
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z" />
|
||||||
|
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z" />
|
||||||
|
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z" />
|
||||||
|
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z" />
|
||||||
|
<glyph unicode="" d="M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z" />
|
||||||
|
<glyph unicode="" d="M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z" />
|
||||||
|
<glyph unicode="" d="M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -116q-25 -17 -43.5 -51.5t-18.5 -65.5v-359z" />
|
||||||
|
<glyph unicode="" d="M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z" />
|
||||||
|
<glyph unicode="" d="M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z" />
|
||||||
|
<glyph unicode="" d="M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q17 18 13.5 41t-22.5 37l-192 136q-19 14 -45 12t-42 -19l-118 -118q-142 101 -268 227t-227 268l118 118q17 17 20 41.5t-11 44.5 l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z" />
|
||||||
|
<glyph unicode="" d="M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-20 0 -35 14.5t-15 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z" />
|
||||||
|
<glyph unicode="" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" />
|
||||||
|
<glyph unicode="" d="M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z" />
|
||||||
|
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
|
||||||
|
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
|
||||||
|
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z" />
|
||||||
|
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z" />
|
||||||
|
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z" />
|
||||||
|
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
|
||||||
|
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
|
||||||
|
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z" />
|
||||||
|
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z" />
|
||||||
|
<glyph unicode="" d="M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z" />
|
||||||
|
<glyph unicode="" d="M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300h200 l-300 -300z" />
|
||||||
|
<glyph unicode="" d="M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104.5t60.5 178.5q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z" />
|
||||||
|
<glyph unicode="" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" />
|
||||||
|
<glyph unicode="" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -11.5t1 -11.5q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" />
|
||||||
|
</font>
|
||||||
|
</defs></svg>
|
After Width: | Height: | Size: 62 KiB |
BIN
static/fonts/glyphicons-halflings-regular.ttf
Normal file
BIN
static/fonts/glyphicons-halflings-regular.ttf
Normal file
Binary file not shown.
BIN
static/fonts/glyphicons-halflings-regular.woff
Normal file
BIN
static/fonts/glyphicons-halflings-regular.woff
Normal file
Binary file not shown.
BIN
static/images/bluepin.gif
Normal file
BIN
static/images/bluepin.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 420 B |
14152
static/js/app.js
Normal file
14152
static/js/app.js
Normal file
File diff suppressed because it is too large
Load diff
BIN
static/js/app.js.gz
Normal file
BIN
static/js/app.js.gz
Normal file
Binary file not shown.
1
static/js/app.min.js
vendored
Normal file
1
static/js/app.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/app.min.js.gz
Normal file
BIN
static/js/app.min.js.gz
Normal file
Binary file not shown.
4
static/js/html5shiv.min.js
vendored
Normal file
4
static/js/html5shiv.min.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
|
||||||
|
*/
|
||||||
|
!function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x<style>"+b+"</style>",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="<xyz></xyz>",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document);
|
BIN
static/js/html5shiv.min.js.gz
Normal file
BIN
static/js/html5shiv.min.js.gz
Normal file
Binary file not shown.
3
static/js/js.cookie-2.2.0.min.js
vendored
Normal file
3
static/js/js.cookie-2.2.0.min.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/*! js-cookie v2.2.0 | MIT */
|
||||||
|
|
||||||
|
!function(e){var n=!1;if("function"==typeof define&&define.amd&&(define(e),n=!0),"object"==typeof exports&&(module.exports=e(),n=!0),!n){var o=window.Cookies,t=window.Cookies=e();t.noConflict=function(){return window.Cookies=o,t}}}(function(){function e(){for(var e=0,n={};e<arguments.length;e++){var o=arguments[e];for(var t in o)n[t]=o[t]}return n}function n(o){function t(n,r,i){var c;if("undefined"!=typeof document){if(arguments.length>1){if("number"==typeof(i=e({path:"/"},t.defaults,i)).expires){var a=new Date;a.setMilliseconds(a.getMilliseconds()+864e5*i.expires),i.expires=a}i.expires=i.expires?i.expires.toUTCString():"";try{c=JSON.stringify(r),/^[\{\[]/.test(c)&&(r=c)}catch(e){}r=o.write?o.write(r,n):encodeURIComponent(r+"").replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),n=(n=(n=encodeURIComponent(n+"")).replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent)).replace(/[\(\)]/g,escape);var s="";for(var f in i)i[f]&&(s+="; "+f,!0!==i[f]&&(s+="="+i[f]));return document.cookie=n+"="+r+s}n||(c={});for(var p=document.cookie?document.cookie.split("; "):[],d=/(%[0-9A-Z]{2})+/g,u=0;u<p.length;u++){var l=p[u].split("="),C=l.slice(1).join("=");this.json||'"'!==C.charAt(0)||(C=C.slice(1,-1));try{var m=l[0].replace(d,decodeURIComponent);if(C=o.read?o.read(C,m):o(C,m)||C.replace(d,decodeURIComponent),this.json)try{C=JSON.parse(C)}catch(e){}if(n===m){c=C;break}n||(c[m]=C)}catch(e){}}return c}}return t.set=t,t.get=function(e){return t.call(t,e)},t.getJSON=function(){return t.apply({json:!0},[].slice.call(arguments))},t.defaults={},t.remove=function(n,o){t(n,"",e(o,{expires:-1}))},t.withConverter=n,t}return n(function(){})});
|
BIN
static/js/js.cookie-2.2.0.min.js.gz
Normal file
BIN
static/js/js.cookie-2.2.0.min.js.gz
Normal file
Binary file not shown.
6
static/js/marked.min.js
vendored
Normal file
6
static/js/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/marked.min.js.gz
Normal file
BIN
static/js/marked.min.js.gz
Normal file
Binary file not shown.
1
static/js/moment.min.js
vendored
Normal file
1
static/js/moment.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/js/moment.min.js.gz
Normal file
BIN
static/js/moment.min.js.gz
Normal file
Binary file not shown.
18
templates/change-password.hamlet
Normal file
18
templates/change-password.hamlet
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<main #main_column .pv2.ph3.mh1>
|
||||||
|
<div .w-100.mw8.center>
|
||||||
|
|
||||||
|
<div .measure.center.pa3.bg-white.ba.br2.b--black-10>
|
||||||
|
|
||||||
|
<form method="post" action="@{ChangePasswordR}">
|
||||||
|
$maybe token <- reqToken req
|
||||||
|
<input type="hidden" name="#{defaultCsrfParamName}" value="#{token}">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label .db.fw6.lh-copy.f6 for="oldpassword">Old Password
|
||||||
|
<input #oldpasword .w-100.pa1.mb2.ba.b--black-20 autofocus required name="oldpassword" type="password" value="">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label .db.fw6.lh-copy.f6 for="newpassword">New Password
|
||||||
|
<input #newpassword .w-100.pa1.mb2.ba.b--black-20 required name="newpassword" type="password">
|
||||||
|
|
||||||
|
<input class="ph3 pv2 input-reset ba b--navy bg-transparent pointer f6 dib mt3 dim" type="submit" value="Save Changes">
|
37
templates/default-layout-wrapper.hamlet
Normal file
37
templates/default-layout-wrapper.hamlet
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
$newline never
|
||||||
|
\<!doctype html>
|
||||||
|
\<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]-->
|
||||||
|
\<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en"> <![endif]-->
|
||||||
|
\<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en"> <![endif]-->
|
||||||
|
\<!--[if gt IE 8]><!-->
|
||||||
|
<html class="no-js" lang="en"> <!--<![endif]-->
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<title>#{pageTitle pc}
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
|
||||||
|
^{pageHead pc}
|
||||||
|
|
||||||
|
\<!--[if lt IE 9]>
|
||||||
|
\<script src="@{StaticR js_html5shiv_min_js}"></script>
|
||||||
|
\<![endif]-->
|
||||||
|
|
||||||
|
<script>document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/, 'js');
|
||||||
|
<script src="@{StaticR js_js_cookie_2_2_0_min_js}">
|
||||||
|
<script>
|
||||||
|
var app =
|
||||||
|
{ csrfHeaderName: "#{ TE.decodeUtf8 $ CI.foldedCase defaultCsrfHeaderName }"
|
||||||
|
, csrfParamName: "#{ defaultCsrfParamName }"
|
||||||
|
, csrfCookieName: "#{ TE.decodeUtf8 defaultCsrfCookieName }"
|
||||||
|
, csrfToken: Cookies.get("#{ TE.decodeUtf8 defaultCsrfCookieName }")
|
||||||
|
, homeR: "@{ HomeR }"
|
||||||
|
, authRlogoutR: "@{ AuthR LogoutR }"
|
||||||
|
, userFilterRFilterSingle: ""
|
||||||
|
, dat: {bmarks : [], bmark: {}, isowner: false, notes: []}
|
||||||
|
};
|
||||||
|
<body .f6.dark-gray.helvetica>
|
||||||
|
^{pageBody pc}
|
34
templates/default-layout.hamlet
Normal file
34
templates/default-layout.hamlet
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<div #content>
|
||||||
|
<header #banner .pv2.ph3.mh1>
|
||||||
|
<div .mw8.center.pb2.bb.br-0.bl-0.bt-0.b--dotted.b--light-silver>
|
||||||
|
<div #logo .fl.light-silver>
|
||||||
|
<a #espial_name .link.f4>espial
|
||||||
|
$maybe userName <- musername
|
||||||
|
<span>
|
||||||
|
(<a class="link" data-username="#{userName}" href="@{UserR (UserNameP userName)}">#{userName}</a>)
|
||||||
|
$maybe user <- muser
|
||||||
|
$if (userPrivacyLock user)
|
||||||
|
<a .dib.no-underline style="height:10px;width:10px" href="@{AccountSettingsR}" title="private profile enabled">🔒
|
||||||
|
|
||||||
|
<!-- <div #timer>#{pageLoadTime} s -->
|
||||||
|
<div .top_menu.fr>
|
||||||
|
|
||||||
|
$maybe userName <- musername
|
||||||
|
$maybe currentroute <- mcurrentRoute
|
||||||
|
<a .link href="@?{(AddViewR, [("next",urlrender currentroute)])}">add url
|
||||||
|
<a .link href="@{AddNoteViewR (UserNameP userName)}">add note
|
||||||
|
<a .link href="@{NotesR (UserNameP userName)}">notes
|
||||||
|
<a .link href="@{AccountSettingsR}">settings
|
||||||
|
<a .link onclick="PS['Main'].logoutE(event)()" href="@{AuthR LogoutR}">
|
||||||
|
log out
|
||||||
|
$nothing
|
||||||
|
<a .link href="@{AuthR LoginR}">
|
||||||
|
log in
|
||||||
|
<div .cf>
|
||||||
|
|
||||||
|
$maybe msg <- mmsg
|
||||||
|
<div .pv2.ph3.mh1>
|
||||||
|
<div .pa2.mw8.center.flex.items-center.justify-center.navy.bg-washed-yellow.ba.b--navy>
|
||||||
|
#{preEscapedToMarkup msg}
|
||||||
|
|
||||||
|
^{widget}
|
86
templates/docs-search.hamlet
Normal file
86
templates/docs-search.hamlet
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<main .pt2.pb5.mh1>
|
||||||
|
<h1 lh-title.mt0 style="font-size:1.35rem">Understanding the Search Syntax
|
||||||
|
|
||||||
|
<h3>Page filters
|
||||||
|
<p .ml3>Searches are scoped to the currently selected page filter.
|
||||||
|
<br>
|
||||||
|
<span>So, given the possible page filters</span>
|
||||||
|
<br><span class="code fw9 bg-light-gray">all ‧ private ‧ public ‧ unread ‧ untagged ‧ starred</span>
|
||||||
|
<br><span>If <span class="code fw9 bg-light-gray">all</span> is currently selected, the search includes <span class="code fw9 bg-light-gray">all</span> bookmarks</span>
|
||||||
|
<br><span>If <span class="code fw9 bg-light-gray">private</span> is currently selected, the search only includes <span class="code fw9 bg-light-gray">private</span> bookmarks, etc..</span>
|
||||||
|
|
||||||
|
<h3>Combine Searches (AND)
|
||||||
|
<p .ml3>Separate terms by a space.
|
||||||
|
<br>For example, <span class="code fw9 bg-light-gray">marathon race</span>
|
||||||
|
|
||||||
|
<h3>Combine Searches (OR)
|
||||||
|
<p .ml3>Put <span class="code fw9 bg-light-gray">|</span> between each search query.
|
||||||
|
<br>For example, <span class="code fw9 bg-light-gray">marathon|race</span>
|
||||||
|
|
||||||
|
<h3>Exclude words from your search
|
||||||
|
<p .ml3>Put <span class="code fw9 bg-light-gray">-</span> in front of a word you want to leave out.
|
||||||
|
<br>For example, <span class="code fw9 bg-light-gray">-car</span>
|
||||||
|
|
||||||
|
<h3>Search for an exact match
|
||||||
|
<p .ml3>Put a word or phrase inside quotes.
|
||||||
|
<br>For example, <span class="code fw9 bg-light-gray">"tallest building"</span>
|
||||||
|
|
||||||
|
<h3>Search on a specific field
|
||||||
|
<p .ml3>
|
||||||
|
Put <span class="code fw9 bg-light-gray"><span class="i">field</span>:</span> in front of the term,
|
||||||
|
where <span class="code fw9 bg-light-gray"><span class="i">field</span></span> is one of the options below
|
||||||
|
<div .ml3>
|
||||||
|
<div .f5.fw6.pb3.underline>Bookmark Search
|
||||||
|
<table .collapse.ba.br2.b--black-10.pv2.ph3>
|
||||||
|
<tbody>
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3>FIELD
|
||||||
|
<td .pv2.ph3>EXAMPLE
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3><span class="code fw9">url
|
||||||
|
<td .pv2.ph3><span class="code fw9">url:youtube.com
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3><span class="code fw9">title
|
||||||
|
<td .pv2.ph3><span class="code fw9">title:"hacker news"
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3><span class="code fw9">description
|
||||||
|
<td .pv2.ph3><span class="code fw9">description:surveys
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3><span class="code fw9">tags
|
||||||
|
<td .pv2.ph3><span class="code fw9">tags:learning
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3><span class="code fw9">after
|
||||||
|
<td .pv2.ph3><span class="code fw9">after:12/31/2018<br>after:2018-12-31
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3><span class="code fw9">before
|
||||||
|
<td .pv2.ph3><span class="code fw9">before:12/31/2019<br>before:2019-12-31
|
||||||
|
|
||||||
|
<div .f5.fw6.pv3.underline>Note Search
|
||||||
|
<table .collapse.ba.br2.b--black-10.pv2.ph3>
|
||||||
|
<tbody>
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3>FIELD
|
||||||
|
<td .pv2.ph3>EXAMPLE
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3><span class="code fw9">title
|
||||||
|
<td .pv2.ph3><span class="code fw9">title:"hacker news"
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3><span class="code fw9">description
|
||||||
|
<td .pv2.ph3><span class="code fw9">description:surveys
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3><span class="code fw9">after
|
||||||
|
<td .pv2.ph3><span class="code fw9">after:12/31/2018<br>after:2018-12-31
|
||||||
|
<tr .striped--light-gray>
|
||||||
|
<td .pv2.ph3><span class="code fw9">before
|
||||||
|
<td .pv2.ph3><span class="code fw9">before:12/31/2019<br>before:2019-12-31
|
||||||
|
|
||||||
|
<h3 .mb0>More Complex Examples
|
||||||
|
<div .ml3.pt3>
|
||||||
|
<span>"youtube" in url, and title of "haskell" or title of "python" and after 12/31/2017
|
||||||
|
<br><span class="mw6 overflow-x-scroll nowrap db mt1 pa2 code fw9 bg-light-gray">url:youtube title:haskell|title:python after:12/31/2017
|
||||||
|
|
||||||
|
<div .ml3.pt3>
|
||||||
|
<span>"hacker news" not in title, and "news", "cnn", "npr" anywhere, or "the guardian" in the description
|
||||||
|
<br><span class="mw6 overflow-x-scroll nowrap db mt1 pa2 code fw9 bg-light-gray">-title:"hacker news" news|cnn|npr|description:"the guardian"
|
||||||
|
|
||||||
|
|
1
templates/homepage.hamlet
Normal file
1
templates/homepage.hamlet
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div #main_column>
|
20
templates/login.hamlet
Normal file
20
templates/login.hamlet
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<main #main_column .pv2.ph3.mh1>
|
||||||
|
<div .w-100.mw8.center>
|
||||||
|
|
||||||
|
<div .measure.center.pa3.bg-white>
|
||||||
|
|
||||||
|
<form method="post" action="@{toParent dbLoginR}">
|
||||||
|
$maybe token <- reqToken req
|
||||||
|
<input type="hidden" name="#{defaultCsrfParamName}" value="#{token}">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label .db.fw6.lh-copy.f6 for="username">Username
|
||||||
|
<input #username .w-100.pa1.mb2.ba.b--black-20 autofocus name="username" type="text" value="">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label .db.fw6.lh-copy.f6 for="password">Password
|
||||||
|
<input #password .w-100.pa1.mb2.ba.b--black-20 name="password" type="password">
|
||||||
|
|
||||||
|
<input class="ph3 pv2 input-reset ba b--navy bg-transparent pointer f6 dib mt3 dim" type="submit" value="Log In">
|
||||||
|
|
||||||
|
<script> document.body.classList.add("bg-near-white");
|
6
templates/note.hamlet
Normal file
6
templates/note.hamlet
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<main #main_column .pv2.ph3.mh1>
|
||||||
|
<div .w-100.mw8.center>
|
||||||
|
|
||||||
|
<div ##{renderEl} .mt3>
|
||||||
|
|
||||||
|
<div .cf>
|
27
templates/notes.hamlet
Normal file
27
templates/notes.hamlet
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<main #main_column .pv2.ph3.mh1>
|
||||||
|
<div .w-100.mw8.center>
|
||||||
|
<div .fr.nt1 style="margin-bottom:.7rem">
|
||||||
|
^{search}
|
||||||
|
|
||||||
|
<span .db .mb3>#{T.append "" (maybe "You have" (const "Found") mquery)} #{bcount} notes:
|
||||||
|
|
||||||
|
^{pager}
|
||||||
|
|
||||||
|
<div .cf>
|
||||||
|
|
||||||
|
<div ##{renderEl} .mt3>
|
||||||
|
|
||||||
|
<div .cf>
|
||||||
|
|
||||||
|
<div .user_footer hidden>
|
||||||
|
^{pager}
|
||||||
|
|
||||||
|
$if (fromIntegral bcount >= limit) || (page > 1)
|
||||||
|
$maybe route <- mroute
|
||||||
|
<div .dib.ml5>
|
||||||
|
<span .silver.mr1>per page:
|
||||||
|
<a .link.light-silver :limit == 20:.nav-active href="@?{(route, [("count", "20")])}"‧>20</a> ‧
|
||||||
|
<a .link.light-silver :limit == 40:.nav-active href="@?{(route, [("count", "40")])}"‧>40</a> ‧
|
||||||
|
<a .link.light-silver :limit == 80:.nav-active href="@?{(route, [("count", "80")])}"‧>80</a> ‧
|
||||||
|
<a .link.light-silver :limit == 120:.nav-active href="@?{(route, [("count", "120")])}"‧>120</a> ‧
|
||||||
|
<a .link.light-silver :limit == 160:.nav-active href="@?{(route, [("count", "160")])}"‧>160</a>
|
13
templates/pager.hamlet
Normal file
13
templates/pager.hamlet
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
$maybe route <- mroute
|
||||||
|
<div #nextprev style="border:0px solid orange">
|
||||||
|
<table style="float:left" border="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
$if fromIntegral bcount >= (limit * page)
|
||||||
|
<td width="80">
|
||||||
|
<a .link.gray #top_earlier href="@?{(route, catMaybes [Just ("page", pack (show (page + 1))), mqueryp])}">
|
||||||
|
« earlier
|
||||||
|
$if page > 1
|
||||||
|
<td width="80">
|
||||||
|
<a .link.gray #top_later href="@?{(route, catMaybes [Just ("page", pack (show (page - 1))), mqueryp])}">
|
||||||
|
later »
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue