Compare commits

..

1 commit
master ... ycss

Author SHA1 Message Date
Yann Esposito (Yogsototh) 0dcc9635db
solarized styling 2019-09-15 19:00:01 +02:00
68 changed files with 9758 additions and 7911 deletions

View file

@ -1,36 +0,0 @@
name: Tests
on:
pull_request:
push:
branches:
- master
workflow_dispatch:
jobs:
build:
name: CI
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
#-macos-latest
#-windows-latest
#resolver:
#- nightly
#- lts-18.7
steps:
- name: Clone project
uses: actions/checkout@v2
- name: Build and run tests
shell: bash
run: |
set -ex
mkdir -p ../_newstack
stack upgrade --force-download --local-bin-path ../_newstack
../_newstack/stack --version
../_newstack/stack test --fast --no-terminal

2
.gitignore vendored
View file

@ -29,5 +29,3 @@ tmp
.cache .cache
tags tags
purs/docset/purescript-local.docset/ purs/docset/purescript-local.docset/
.ghc.environment.x86_64-linux-8.6.5
.vscode

View file

@ -1,6 +1,3 @@
_DOCKER:=docker
_DOCKER_COMPOSE:=docker compose
.PHONY: clean build .PHONY: clean build
all: build all: build
@ -29,8 +26,9 @@ migrate-createdb:
serve: serve:
@stack exec espial -- +RTS -T @stack exec espial -- +RTS -T
_ESPIAL_PS_ID = $$($(_DOCKER_COMPOSE) ps -q espial) _ESPIAL_PS_ID = $$(docker-compose ps -q espial)
_LOCAL_INSTALL_PATH = $$(stack path | grep local-install-root | awk -e '{print $$2}') _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 docker-compose-build: build
@rm -Rf dist && mkdir -p dist @rm -Rf dist && mkdir -p dist
@ -38,20 +36,22 @@ docker-compose-build: build
@cp -R static dist @cp -R static dist
@rm -Rf dist/static/tmp @rm -Rf dist/static/tmp
@cp -R config dist @cp -R config dist
@$(_DOCKER_COMPOSE) build espial @mkdir -p dist/ekg/assets
@cp -R $(_EKG_ASSETS_PATH) dist/ekg
@docker-compose build espial
docker-compose-up: docker-compose-up:
@$(_DOCKER_COMPOSE) up --no-deps --no-build espial @docker-compose up --no-deps --no-build espial
docker-compose-down: docker-compose-down:
@$(_DOCKER_COMPOSE) down @docker-compose down
docker-compose-up-d: docker-compose-up-d:
@$(_DOCKER_COMPOSE) up --no-deps --no-build -d espial @docker-compose up --no-deps --no-build -d espial
docker-compose-pull: docker-compose-pull:
@$(_DOCKER_COMPOSE) pull espial @docker-compose pull espial
docker-compose-push: docker-compose-push:
@docker tag localhost/espial:espial $(HUB_REPO)/espial:espial @docker tag localhost/espial:espial $(HUB_REPO)/espial:espial
@$(_DOCKER_COMPOSE) push espial @docker-compose push espial
docker-espial-logs: docker-espial-logs:
@$(_DOCKER) logs -f --since `date -u +%FT%TZ` $(_ESPIAL_PS_ID) @docker logs -f --since `date -u +%FT%TZ` $(_ESPIAL_PS_ID)
docker-espial-shell: docker-espial-shell:
@$(docker_espial) sh @$(docker_espial) sh
@ -61,7 +61,7 @@ ifeq ($(_HUB_REPO),)
_HUB_REPO := "localhost" _HUB_REPO := "localhost"
endif endif
docker_espial = $(_DOCKER_COMPOSE) exec espial docker_espial = docker-compose exec espial
clean: clean:
@stack clean @stack clean

View file

@ -8,24 +8,18 @@ The bookmarks are stored in a sqlite3 database, for ease of deployment & mainten
The easist way for logged-in users to add bookmarks, is with the "bookmarklet", found on the Settings page. The easist way for logged-in users to add bookmarks, is with the "bookmarklet", found on the Settings page.
Also, see the android app for adding bookmarks via an Android Share intent https://github.com/jonschoning/espial-share-android
## demo server ## demo server
log in — username: demo password: demo log in — username: demo password: demo
https://esp.ae8.org/u:demo https://esp.ae8.org/u:demo
![jpg](https://i.imgur.com/jdnV93c.png) ![jpg](https://i.imgur.com/XikHLua.png)
## Docker Setup
see https://github.com/jonschoning/espial-docker
## Server Setup (from source) ## Server Setup (from source)
1. Install the Stack executable here: 1. [Install Stack](https://haskell-lang.org/get-started)
- https://tech.fpcomplete.com/haskell/get-started - On POSIX systems, this is usually `curl -sSL https://get.haskellstack.org/ | sh`
2. Build executables 2. Build executables
@ -45,36 +39,25 @@ see https://github.com/jonschoning/espial-docker
stack exec migration -- createuser --conn espial.sqlite3 --userName myusername --userPassword myuserpassword stack exec migration -- createuser --conn espial.sqlite3 --userName myusername --userPassword myuserpassword
``` ```
5. Import a pinboard bookmark file for a user (optional) 5. Import a bookmark file for a user (optional)
``` ```
stack exec migration -- importbookmarks --conn espial.sqlite3 --userName myusername --bookmarkFile sample-bookmarks.json stack exec migration -- importbookmarks --conn espial.sqlite3 --userName myusername --bookmarkFile sample-bookmarks.json
``` ```
6. Import a firefox bookmark file for a user (optional) 6. Start a production server:
``` ```
stack exec migration -- importfirefoxbookmarks --conn espial.sqlite3 --userName myusername --bookmarkFile firefox-bookmarks.json stack exec espial -- +RTS -T
``` ```
7. Start a production server: see `config/settings.yml` for changing default run-time parameters / environment variables
``` default app http port: `3000`
stack exec espial
```
### Configuration default ekg http port: `8000`
See `config/settings.yml` for changing default run-time parameters & environment variables. ssl: use reverse proxy
- `config/settings.yml` is embedded into the app executable when compiled, so after changing `config/settings.yml`, run `stack build` again to apply the new settings.
- `config/settings.yml` values formatted like `_env:ENV_VAR_NAME:default_value` can be
overridden by the specified environment variable.
- Example
- `_env:PORT:3000`
- environment variable `PORT`
- default app http port: `3000`
SSL: use reverse proxy
## Development ## Development
@ -93,7 +76,7 @@ SSL: use reverse proxy
- See `purs/` folder - See `purs/` folder
## Import Bookmark file format (pinboard compatible format) ## Import Bookmark file format
see `sample-bookmarks.json`, which contains a JSON array, each line containing a `FileBookmark` object. see `sample-bookmarks.json`, which contains a JSON array, each line containing a `FileBookmark` object.

View file

@ -1,5 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Please report vulnerabilities to jonschoning@gmail.com

View file

@ -12,64 +12,25 @@ import ClassyPrelude
import Lens.Micro import Lens.Micro
import Options.Generic import Options.Generic
import qualified Options.Applicative as OA
import qualified Data.Text as T
data Password
= PasswordText Text
| PasswordFile FilePath
deriving (Show, Read)
parsePassword :: OA.Parser Password
parsePassword = passwordText <|> passwordFile
where
passwordText = PasswordText <$> OA.strOption
( OA.long "userPassword"
<> OA.metavar "PASSWORD"
<> OA.help "Password in plain-text"
)
passwordFile = PasswordFile <$> OA.strOption
( OA.long "userPasswordFile"
<> OA.metavar "FILE"
<> OA.help "Password file"
)
instance ParseFields Password
instance ParseRecord Password where
parseRecord = fmap getOnly parseRecord
instance ParseField Password where
parseField _ _ _ _ = parsePassword
data MigrationOpts data MigrationOpts
= CreateDB { conn :: Text } = CreateDB { conn :: Text}
| CreateUser { conn :: Text | CreateUser { conn :: Text
, userName :: Text , userName :: Text
, userPassword :: Password , userPassword :: Text
, privateDefault :: Maybe Bool , userApiToken :: Maybe Text }
, archiveDefault :: Maybe Bool
, privacyLock :: Maybe Bool }
| CreateApiKey { conn :: Text
, userName :: Text }
| DeleteUser { conn :: Text | DeleteUser { conn :: Text
, userName :: Text } , userName :: Text}
| DeleteApiKey { conn :: Text
, userName :: Text }
| ImportBookmarks { conn :: Text | ImportBookmarks { conn :: Text
, userName :: Text , userName :: Text
, bookmarkFile :: FilePath } , bookmarkFile :: FilePath}
| ImportFirefoxBookmarks { conn :: Text
, userName :: Text
, bookmarkFile :: FilePath }
| ExportBookmarks { conn :: Text | ExportBookmarks { conn :: Text
, userName :: Text , userName :: Text
, bookmarkFile :: FilePath } , bookmarkFile :: FilePath}
| ImportNotes { conn :: Text | ImportNotes { conn :: Text
, userName :: Text , userName :: Text
, noteDirectory :: FilePath } , noteDirectory :: FilePath}
| PrintMigrateDB { conn :: Text } | PrintMigrateDB { conn :: Text}
deriving (Generic, Show) deriving (Generic, Show)
instance ParseRecord MigrationOpts instance ParseRecord MigrationOpts
@ -78,97 +39,54 @@ main :: IO ()
main = do main = do
args <- getRecord "Migrations" args <- getRecord "Migrations"
case args of case args of
PrintMigrateDB {..} -> PrintMigrateDB conn ->
P.runSqlite conn dumpMigration P.runSqlite conn dumpMigration
CreateDB {..} -> do CreateDB conn -> do
let connInfo = P.mkSqliteConnectionInfo conn let connInfo = P.mkSqliteConnectionInfo conn
& set P.fkEnabled False & set P.fkEnabled False
P.runSqliteInfo connInfo runMigrations P.runSqliteInfo connInfo runMigrations
CreateUser{..} -> CreateUser conn uname upass utoken ->
P.runSqlite conn $ do P.runSqlite conn $ do
passwordText <- liftIO . fmap T.strip $ case userPassword of hash' <- liftIO (hashPassword upass)
PasswordText s -> pure s
PasswordFile f -> readFileUtf8 f
hash' <- liftIO (hashPassword passwordText)
void $ P.upsertBy void $ P.upsertBy
(UniqueUserName userName) (UniqueUserName uname)
(User userName hash' Nothing False False False) (User uname hash' utoken False False False)
[ UserPasswordHash P.=. hash' [ UserPasswordHash P.=. hash'
, UserPrivateDefault P.=. fromMaybe False privateDefault , UserApiToken P.=. utoken
, UserArchiveDefault P.=. fromMaybe False archiveDefault , UserPrivateDefault P.=. False
, UserPrivacyLock P.=. fromMaybe False privacyLock , UserArchiveDefault P.=. False
, UserPrivacyLock P.=. False
] ]
pure () :: DB () pure () :: DB ()
CreateApiKey {..} -> DeleteUser conn uname ->
P.runSqlite conn $ do P.runSqlite conn $ do
apiKey@(ApiKey plainKey) <- liftIO generateApiKey muser <- P.getBy (UniqueUserName uname)
muser <- P.getBy (UniqueUserName userName)
case muser of case muser of
Nothing -> liftIO (print (userName ++ " not found")) Nothing -> liftIO (print (uname ++ "not found"))
Just (P.Entity uid _) -> do Just (P.Entity uid _) -> do
-- API key is only displayed once after creation, P.deleteCascade uid
-- since it is stored in hashed form.
let hashedKey = hashApiKey apiKey
P.update uid [ UserApiToken P.=. Just hashedKey ]
liftIO $ print plainKey
DeleteApiKey {..} ->
P.runSqlite conn $ do
muser <- P.getBy (UniqueUserName userName)
case muser of
Nothing -> liftIO (print (userName ++ " not found"))
Just (P.Entity uid _) -> do
P.update uid [ UserApiToken P.=. Nothing ]
DeleteUser {..} ->
P.runSqlite conn $ do
muser <- P.getBy (UniqueUserName userName)
case muser of
Nothing -> liftIO (print (userName ++ "not found"))
Just (P.Entity uid _) -> do
P.delete uid
pure () :: DB () pure () :: DB ()
ExportBookmarks {..} -> ImportBookmarks conn uname file ->
P.runSqlite conn $ do P.runSqlite conn $ do
muser <- P.getBy (UniqueUserName userName) muser <- P.getBy (UniqueUserName uname)
case muser of case muser of
Just (P.Entity uid _) -> exportFileBookmarks uid bookmarkFile Just (P.Entity uid _) -> insertFileBookmarks uid file
Nothing -> liftIO (print (userName ++ "not found")) Nothing -> liftIO (print (uname ++ "not found"))
ImportBookmarks {..} -> ExportBookmarks conn uname file ->
P.runSqlite conn $ do P.runSqlite conn $ do
muser <- P.getBy (UniqueUserName userName) muser <- P.getBy (UniqueUserName uname)
case muser of case muser of
Just (P.Entity uid _) -> do Just (P.Entity uid _) -> exportFileBookmarks uid file
result <- insertFileBookmarks uid bookmarkFile Nothing -> liftIO (print (uname ++ "not found"))
case result of
Left e -> liftIO (print e)
Right n -> liftIO (print (show n ++ " bookmarks imported."))
Nothing -> liftIO (print (userName ++ "not found"))
ImportNotes conn uname dir ->
ImportFirefoxBookmarks {..} ->
P.runSqlite conn $ do P.runSqlite conn $ do
muser <- P.getBy (UniqueUserName userName) muser <- P.getBy (UniqueUserName uname)
case muser of case muser of
Just (P.Entity uid _) -> do Just (P.Entity uid _) -> insertDirFileNotes uid dir
result <- insertFFBookmarks uid bookmarkFile Nothing -> liftIO (print (uname ++ "not found"))
case result of
Left e -> liftIO (print e)
Right n -> liftIO (print (show n ++ " bookmarks imported."))
Nothing -> liftIO (print (userName ++ "not found"))
ImportNotes {..} ->
P.runSqlite conn $ do
muser <- P.getBy (UniqueUserName userName)
case muser of
Just (P.Entity uid _) -> do
result <- insertDirFileNotes uid noteDirectory
case result of
Left e -> liftIO (print e)
Right n -> liftIO (print (show n ++ " notes imported."))
Nothing -> liftIO (print (userName ++ "not found"))

View file

@ -1,25 +1,3 @@
__v0.0.15__
Avoid using external static/tmp folder for generated static files
__v0.0.14__
upgrade to purescript v0.15
increase bookmarklet window height
__v0.0.13__
add setting ALLOW_NON_HTTP_URL_SCHEMES (default false)
__v0.0.12__
update to ghc9
__v0.0.11__
add api key auth.
add CreateApiKey/DeleteApiKey commands to executable 'migration'
__v0.0.10__
update purescript&package versions
__v0.0.9__
(rolling releases)
__v0.0.7__ __v0.0.7__

View file

@ -8,7 +8,6 @@
-- notes -- notes
!/#UserNameP/notes NotesR GET !/#UserNameP/notes NotesR GET
!/#UserNameP/notes/add AddNoteViewR GET !/#UserNameP/notes/add AddNoteViewR GET
!/notes/add AddNoteSlimViewR GET
!/#UserNameP/notes/feed.xml NotesFeedR GET !/#UserNameP/notes/feed.xml NotesFeedR GET
!/#UserNameP/notes/#NtSlug NoteR GET !/#UserNameP/notes/#NtSlug NoteR GET
!/api/note/add AddNoteR POST !/api/note/add AddNoteR POST
@ -20,11 +19,7 @@
!/#UserNameP/#SharedP UserSharedR GET !/#UserNameP/#SharedP UserSharedR GET
!/#UserNameP/#FilterP UserFilterR GET !/#UserNameP/#FilterP UserFilterR GET
!/#UserNameP/#TagsP UserTagsR GET !/#UserNameP/#TagsP UserTagsR GET
!/#UserNameP/feed.xml UserFeedR GET !/#UserNameP/feed.xml UserFeedR GET
!/#UserNameP/#SharedP/feed.xml UserFeedSharedR GET
!/#UserNameP/#FilterP/feed.xml UserFeedFilterR GET
!/#UserNameP/#TagsP/feed.xml UserFeedTagsR GET
-- settings -- settings
/Settings AccountSettingsR GET /Settings AccountSettingsR GET
@ -37,11 +32,6 @@ api/accountSettings EditAccountSettingsR POST
/add AddViewR GET /add AddViewR GET
api/add AddR POST api/add AddR POST
-- api
api/lookuptitle LookupTitleR POST
api/tagcloud UserTagCloudR POST
api/tagcloudmode UserTagCloudModeR POST
-- edit -- edit
/bm/#Int64 DeleteR DELETE /bm/#Int64 DeleteR DELETE
/bm/#Int64/read ReadR POST /bm/#Int64/read ReadR POST
@ -49,4 +39,4 @@ api/tagcloudmode UserTagCloudModeR POST
/bm/#Int64/unstar UnstarR POST /bm/#Int64/unstar UnstarR POST
-- doc -- doc
/docs/search DocsSearchR GET /docs/search DocsSearchR GET

View file

@ -18,8 +18,8 @@ ip-from-header: "_env:IP_FROM_HEADER:false"
# Optional values with the following production defaults. # Optional values with the following production defaults.
# In development, they default to the inverse. # In development, they default to the inverse.
# #
detailed-logging: "_env:DETAILED_LOGGING" # false # detailed-logging: false
should-log-all: "_env:SHOULD_LOG_ALL" # false # should-log-all: false
# reload-templates: false # reload-templates: false
# mutable-static: false # mutable-static: false
# skip-combining: false # skip-combining: false
@ -37,11 +37,7 @@ database:
copyright: Insert copyright statement here copyright: Insert copyright statement here
#analytics: UA-YOURCODE #analytics: UA-YOURCODE
archive-socks-proxy-host: "_env:ARCHIVE_SOCKS_PROXY_HOST" ekg-host: "_env:EKG_HOST:0.0.0.0"
archive-socks-proxy-port: "_env:ARCHIVE_SOCKS_PROXY_PORT" ekg-port: "_env:EKG_PORT:8000"
source-code-uri: "_env:SOURCE_CODE_URI:https://github.com/jonschoning/espial" source-code-uri: "https://github.com/jonschoning/espial"
ssl-only: "_env:SSL_ONLY" # false
allow-non-http-url-schemes: "_env:ALLOW_NON_HTTP_URL_SCHEMES:false"

View file

@ -7,16 +7,10 @@ services:
dockerfile: ../Dockerfile dockerfile: ../Dockerfile
ports: ports:
- "3000:3000" - "3000:3000"
- "8000:8000"
volumes: volumes:
- '$APPDATA:/app/data' - '$APPDATA:/app/data'
network_mode: host
environment: environment:
- IP_FROM_HEADER=true - IP_FROM_HEADER=true
- SQLITE_DATABASE=/app/data/espial.sqlite3 - SQLITE_DATABASE=/app/data/espial.sqlite3
# - SSL_ONLY=false - ekg_datadir=ekg
# - DETAILED_LOGGING=false
# - SHOULD_LOG_ALL=false
# - ARCHIVE_SOCKS_PROXY_HOST=localhost
# - ARCHIVE_SOCKS_PROXY_PORT=8888
# - SOURCE_CODE_URI=https://github.com/jonschoning/espial
# - ALLOW_NON_HTTP_URL_SCHEMES=false

View file

@ -1,11 +1,13 @@
cabal-version: 1.12 cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.35.0. -- This file has been generated from package.yaml by hpack version 0.31.2.
-- --
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
--
-- hash: da944088abb7ae887d67efd710c100bdbd5587072c6ddcfdc5d05392e7509d85
name: espial name: espial
version: 0.0.15 version: 0.0.8
synopsis: Espial is an open-source, web-based bookmarking server. synopsis: Espial is an open-source, web-based bookmarking server.
description: . description: .
Espial is an open-source, web-based bookmarking server. Espial is an open-source, web-based bookmarking server.
@ -62,6 +64,7 @@ extra-source-files:
purs/src/App.purs purs/src/App.purs
purs/src/Globals.js purs/src/Globals.js
purs/src/Globals.purs purs/src/Globals.purs
purs/src/Main.js
purs/src/Main.purs purs/src/Main.purs
purs/src/Marked.js purs/src/Marked.js
purs/src/Marked.purs purs/src/Marked.purs
@ -76,7 +79,6 @@ extra-source-files:
purs/src/Component/NNote.purs purs/src/Component/NNote.purs
purs/src/Component/RawHtml.js purs/src/Component/RawHtml.js
purs/src/Component/RawHtml.purs purs/src/Component/RawHtml.purs
purs/src/Component/TagCloud.purs
purs/test/Main.purs purs/test/Main.purs
source-repository head source-repository head
@ -120,86 +122,48 @@ library
Paths_espial Paths_espial
hs-source-dirs: hs-source-dirs:
src src
default-extensions: 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
BangPatterns
BlockArguments
CPP
ConstraintKinds
DataKinds
DeriveDataTypeable
DeriveGeneric
DerivingStrategies
EmptyDataDecls
FlexibleContexts
FlexibleInstances
GADTs
GeneralizedNewtypeDeriving
InstanceSigs
KindSignatures
LambdaCase
MultiParamTypeClasses
MultiWayIf
NoImplicitPrelude
OverloadedStrings
PolyKinds
PolymorphicComponents
PartialTypeSignatures
QuasiQuotes
Rank2Types
RankNTypes
RecordWildCards
ScopedTypeVariables
StandaloneDeriving
TemplateHaskell
TupleSections
TypeApplications
TypeFamilies
TypeOperators
TypeSynonymInstances
UndecidableInstances
ViewPatterns
build-depends: build-depends:
aeson >=1.4 aeson >=1.4
, attoparsec , attoparsec
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5 , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
, base64
, bcrypt >=0.0.8 , bcrypt >=0.0.8
, blaze-html >=0.9 && <1.0 , blaze-html >=0.9 && <1.0
, bytestring >=0.9 && <0.14 , bytestring >=0.9 && <0.11
, case-insensitive , case-insensitive
, classy-prelude >=1.4 && <1.6 , classy-prelude >=1.4 && <1.6
, classy-prelude-conduit >=1.4 && <1.6 , classy-prelude-conduit >=1.4 && <1.6
, classy-prelude-yesod >=1.4 && <1.6 , classy-prelude-yesod >=1.4 && <1.6
, conduit >=1.0 && <2.0 , conduit >=1.0 && <2.0
, connection
, containers , containers
, cryptohash-sha256
, data-default , data-default
, directory >=1.1 && <1.4 , directory >=1.1 && <1.4
, ekg
, ekg-core
, entropy , entropy
, esqueleto , esqueleto
, fast-logger >=2.2 && <4 , fast-logger >=2.2 && <2.5
, file-embed , file-embed
, foreign-store , foreign-store
, hjsmin >=0.1 && <0.3 , hjsmin >=0.1 && <0.3
, hscolour , hscolour
, html-entities
, http-api-data >=0.3.4 , http-api-data >=0.3.4
, http-client , http-client
, http-client-tls ==0.3.* , http-client-tls >=0.3 && <0.4
, http-conduit ==2.3.* , http-conduit >=2.3 && <2.4
, http-types , http-types
, iso8601-time >=0.1.3 , iso8601-time >=0.1.3
, microlens , microlens
, monad-logger ==0.3.* , monad-logger >=0.3 && <0.4
, monad-metrics
, mtl , mtl
, parser-combinators , parser-combinators
, persistent >=2.8 && <2.14 , persistent >=2.8 && <2.10
, persistent-sqlite >=2.6.2 , persistent-sqlite >=2.6.2
, persistent-template >=2.5 && <2.13 , persistent-template >=2.5 && <2.9
, pretty-show , pretty-show
, safe , safe
, shakespeare ==2.0.* , shakespeare >=2.0 && <2.1
, template-haskell , template-haskell
, text >=0.11 && <2.0 , text >=0.11 && <2.0
, time , time
@ -207,22 +171,23 @@ library
, unordered-containers , unordered-containers
, vector , vector
, wai , wai
, wai-extra >=3.0 && <3.2 , wai-extra >=3.0 && <3.1
, wai-logger , wai-logger >=2.2 && <2.4
, warp >=3.0 && <3.4 , wai-middleware-metrics
, warp >=3.0 && <3.3
, yaml >=0.8 && <0.12 , yaml >=0.8 && <0.12
, yesod >=1.6 && <1.8 , yesod >=1.6 && <1.7
, yesod-auth >=1.6 && <1.8 , yesod-auth >=1.6 && <1.7
, yesod-core >=1.6 && <1.8 , yesod-core >=1.6 && <1.7
, yesod-form >=1.6 && <1.8 , yesod-form >=1.6 && <1.7
, yesod-newsfeed >=1.6 && <1.8 , yesod-newsfeed >=1.6 && <1.7
, yesod-static >=1.6 && <1.8 , yesod-static >=1.6 && <1.7
default-language: Haskell2010
if (flag(dev)) || (flag(library-only)) if (flag(dev)) || (flag(library-only))
ghc-options: -Wall -fwarn-tabs -O0 ghc-options: -Wall -fwarn-tabs -O0
cpp-options: -DDEVELOPMENT cpp-options: -DDEVELOPMENT
else else
ghc-options: -Wall -fwarn-tabs -O2 ghc-options: -Wall -fwarn-tabs -O2
default-language: Haskell2010
executable espial executable espial
main-is: main.hs main-is: main.hs
@ -231,88 +196,50 @@ executable espial
Paths_espial Paths_espial
hs-source-dirs: hs-source-dirs:
app app
default-extensions: 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
BangPatterns
BlockArguments
CPP
ConstraintKinds
DataKinds
DeriveDataTypeable
DeriveGeneric
DerivingStrategies
EmptyDataDecls
FlexibleContexts
FlexibleInstances
GADTs
GeneralizedNewtypeDeriving
InstanceSigs
KindSignatures
LambdaCase
MultiParamTypeClasses
MultiWayIf
NoImplicitPrelude
OverloadedStrings
PolyKinds
PolymorphicComponents
PartialTypeSignatures
QuasiQuotes
Rank2Types
RankNTypes
RecordWildCards
ScopedTypeVariables
StandaloneDeriving
TemplateHaskell
TupleSections
TypeApplications
TypeFamilies
TypeOperators
TypeSynonymInstances
UndecidableInstances
ViewPatterns
ghc-options: -threaded -rtsopts -with-rtsopts=-N ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: build-depends:
aeson >=1.4 aeson >=1.4
, attoparsec , attoparsec
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5 , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
, base64
, bcrypt >=0.0.8 , bcrypt >=0.0.8
, blaze-html >=0.9 && <1.0 , blaze-html >=0.9 && <1.0
, bytestring >=0.9 && <0.14 , bytestring >=0.9 && <0.11
, case-insensitive , case-insensitive
, classy-prelude >=1.4 && <1.6 , classy-prelude >=1.4 && <1.6
, classy-prelude-conduit >=1.4 && <1.6 , classy-prelude-conduit >=1.4 && <1.6
, classy-prelude-yesod >=1.4 && <1.6 , classy-prelude-yesod >=1.4 && <1.6
, conduit >=1.0 && <2.0 , conduit >=1.0 && <2.0
, connection
, containers , containers
, cryptohash-sha256
, data-default , data-default
, directory >=1.1 && <1.4 , directory >=1.1 && <1.4
, ekg
, ekg-core
, entropy , entropy
, espial , espial
, esqueleto , esqueleto
, fast-logger >=2.2 && <4 , fast-logger >=2.2 && <2.5
, file-embed , file-embed
, foreign-store , foreign-store
, hjsmin >=0.1 && <0.3 , hjsmin >=0.1 && <0.3
, hscolour , hscolour
, html-entities
, http-api-data >=0.3.4 , http-api-data >=0.3.4
, http-client , http-client
, http-client-tls ==0.3.* , http-client-tls >=0.3 && <0.4
, http-conduit ==2.3.* , http-conduit >=2.3 && <2.4
, http-types , http-types
, iso8601-time >=0.1.3 , iso8601-time >=0.1.3
, microlens , microlens
, monad-logger ==0.3.* , monad-logger >=0.3 && <0.4
, monad-metrics
, mtl , mtl
, parser-combinators , parser-combinators
, persistent >=2.8 && <2.14 , persistent >=2.8 && <2.10
, persistent-sqlite >=2.6.2 , persistent-sqlite >=2.6.2
, persistent-template >=2.5 && <2.13 , persistent-template >=2.5 && <2.9
, pretty-show , pretty-show
, safe , safe
, shakespeare ==2.0.* , shakespeare >=2.0 && <2.1
, template-haskell , template-haskell
, text >=0.11 && <2.0 , text >=0.11 && <2.0
, time , time
@ -320,19 +247,20 @@ executable espial
, unordered-containers , unordered-containers
, vector , vector
, wai , wai
, wai-extra >=3.0 && <3.2 , wai-extra >=3.0 && <3.1
, wai-logger , wai-logger >=2.2 && <2.4
, warp >=3.0 && <3.4 , wai-middleware-metrics
, warp >=3.0 && <3.3
, yaml >=0.8 && <0.12 , yaml >=0.8 && <0.12
, yesod >=1.6 && <1.8 , yesod >=1.6 && <1.7
, yesod-auth >=1.6 && <1.8 , yesod-auth >=1.6 && <1.7
, yesod-core >=1.6 && <1.8 , yesod-core >=1.6 && <1.7
, yesod-form >=1.6 && <1.8 , yesod-form >=1.6 && <1.7
, yesod-newsfeed >=1.6 && <1.8 , yesod-newsfeed >=1.6 && <1.7
, yesod-static >=1.6 && <1.8 , yesod-static >=1.6 && <1.7
default-language: Haskell2010
if flag(library-only) if flag(library-only)
buildable: False buildable: False
default-language: Haskell2010
executable migration executable migration
main-is: Main.hs main-is: Main.hs
@ -340,90 +268,51 @@ executable migration
Paths_espial Paths_espial
hs-source-dirs: hs-source-dirs:
app/migration app/migration
default-extensions: 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
BangPatterns
BlockArguments
CPP
ConstraintKinds
DataKinds
DeriveDataTypeable
DeriveGeneric
DerivingStrategies
EmptyDataDecls
FlexibleContexts
FlexibleInstances
GADTs
GeneralizedNewtypeDeriving
InstanceSigs
KindSignatures
LambdaCase
MultiParamTypeClasses
MultiWayIf
NoImplicitPrelude
OverloadedStrings
PolyKinds
PolymorphicComponents
PartialTypeSignatures
QuasiQuotes
Rank2Types
RankNTypes
RecordWildCards
ScopedTypeVariables
StandaloneDeriving
TemplateHaskell
TupleSections
TypeApplications
TypeFamilies
TypeOperators
TypeSynonymInstances
UndecidableInstances
ViewPatterns
ghc-options: -threaded -rtsopts -with-rtsopts=-N ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: build-depends:
aeson >=1.4 aeson >=1.4
, attoparsec , attoparsec
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5 , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
, base64
, bcrypt >=0.0.8 , bcrypt >=0.0.8
, blaze-html >=0.9 && <1.0 , blaze-html >=0.9 && <1.0
, bytestring >=0.9 && <0.14 , bytestring >=0.9 && <0.11
, case-insensitive , case-insensitive
, classy-prelude >=1.4 && <1.6 , classy-prelude >=1.4 && <1.6
, classy-prelude-conduit >=1.4 && <1.6 , classy-prelude-conduit >=1.4 && <1.6
, classy-prelude-yesod >=1.4 && <1.6 , classy-prelude-yesod >=1.4 && <1.6
, conduit >=1.0 && <2.0 , conduit >=1.0 && <2.0
, connection
, containers , containers
, cryptohash-sha256
, data-default , data-default
, directory >=1.1 && <1.4 , directory >=1.1 && <1.4
, ekg
, ekg-core
, entropy , entropy
, espial , espial
, esqueleto , esqueleto
, fast-logger >=2.2 && <4 , fast-logger >=2.2 && <2.5
, file-embed , file-embed
, foreign-store , foreign-store
, hjsmin >=0.1 && <0.3 , hjsmin >=0.1 && <0.3
, hscolour , hscolour
, html-entities
, http-api-data >=0.3.4 , http-api-data >=0.3.4
, http-client , http-client
, http-client-tls ==0.3.* , http-client-tls >=0.3 && <0.4
, http-conduit ==2.3.* , http-conduit >=2.3 && <2.4
, http-types , http-types
, iso8601-time >=0.1.3 , iso8601-time >=0.1.3
, microlens , microlens
, monad-logger ==0.3.* , monad-logger >=0.3 && <0.4
, monad-metrics
, mtl , mtl
, optparse-applicative
, optparse-generic >=1.2.3 , optparse-generic >=1.2.3
, parser-combinators , parser-combinators
, persistent >=2.8 && <2.14 , persistent >=2.8 && <2.10
, persistent-sqlite >=2.6.2 , persistent-sqlite >=2.6.2
, persistent-template >=2.5 && <2.13 , persistent-template >=2.5 && <2.9
, pretty-show , pretty-show
, safe , safe
, shakespeare ==2.0.* , shakespeare >=2.0 && <2.1
, template-haskell , template-haskell
, text >=0.11 && <2.0 , text >=0.11 && <2.0
, time , time
@ -431,19 +320,20 @@ executable migration
, unordered-containers , unordered-containers
, vector , vector
, wai , wai
, wai-extra >=3.0 && <3.2 , wai-extra >=3.0 && <3.1
, wai-logger , wai-logger >=2.2 && <2.4
, warp >=3.0 && <3.4 , wai-middleware-metrics
, warp >=3.0 && <3.3
, yaml >=0.8 && <0.12 , yaml >=0.8 && <0.12
, yesod >=1.6 && <1.8 , yesod >=1.6 && <1.7
, yesod-auth >=1.6 && <1.8 , yesod-auth >=1.6 && <1.7
, yesod-core >=1.6 && <1.8 , yesod-core >=1.6 && <1.7
, yesod-form >=1.6 && <1.8 , yesod-form >=1.6 && <1.7
, yesod-newsfeed >=1.6 && <1.8 , yesod-newsfeed >=1.6 && <1.7
, yesod-static >=1.6 && <1.8 , yesod-static >=1.6 && <1.7
default-language: Haskell2010
if flag(library-only) if flag(library-only)
buildable: False buildable: False
default-language: Haskell2010
test-suite test test-suite test
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
@ -455,89 +345,51 @@ test-suite test
Paths_espial Paths_espial
hs-source-dirs: hs-source-dirs:
test test
default-extensions: 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
BangPatterns
BlockArguments
CPP
ConstraintKinds
DataKinds
DeriveDataTypeable
DeriveGeneric
DerivingStrategies
EmptyDataDecls
FlexibleContexts
FlexibleInstances
GADTs
GeneralizedNewtypeDeriving
InstanceSigs
KindSignatures
LambdaCase
MultiParamTypeClasses
MultiWayIf
NoImplicitPrelude
OverloadedStrings
PolyKinds
PolymorphicComponents
PartialTypeSignatures
QuasiQuotes
Rank2Types
RankNTypes
RecordWildCards
ScopedTypeVariables
StandaloneDeriving
TemplateHaskell
TupleSections
TypeApplications
TypeFamilies
TypeOperators
TypeSynonymInstances
UndecidableInstances
ViewPatterns
ghc-options: -Wall ghc-options: -Wall
build-depends: build-depends:
aeson >=1.4 aeson >=1.4
, attoparsec , attoparsec
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5 , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
, base64
, bcrypt >=0.0.8 , bcrypt >=0.0.8
, blaze-html >=0.9 && <1.0 , blaze-html >=0.9 && <1.0
, bytestring >=0.9 && <0.14 , bytestring >=0.9 && <0.11
, case-insensitive , case-insensitive
, classy-prelude >=1.4 && <1.6 , classy-prelude >=1.4 && <1.6
, classy-prelude-conduit >=1.4 && <1.6 , classy-prelude-conduit >=1.4 && <1.6
, classy-prelude-yesod >=1.4 && <1.6 , classy-prelude-yesod >=1.4 && <1.6
, conduit >=1.0 && <2.0 , conduit >=1.0 && <2.0
, connection
, containers , containers
, cryptohash-sha256
, data-default , data-default
, directory >=1.1 && <1.4 , directory >=1.1 && <1.4
, ekg
, ekg-core
, entropy , entropy
, espial , espial
, esqueleto , esqueleto
, fast-logger >=2.2 && <4 , fast-logger >=2.2 && <2.5
, file-embed , file-embed
, foreign-store , foreign-store
, hjsmin >=0.1 && <0.3 , hjsmin >=0.1 && <0.3
, hscolour , hscolour
, hspec >=2.0.0 , hspec >=2.0.0
, html-entities
, http-api-data >=0.3.4 , http-api-data >=0.3.4
, http-client , http-client
, http-client-tls ==0.3.* , http-client-tls >=0.3 && <0.4
, http-conduit ==2.3.* , http-conduit >=2.3 && <2.4
, http-types , http-types
, iso8601-time >=0.1.3 , iso8601-time >=0.1.3
, microlens , microlens
, monad-logger ==0.3.* , monad-logger >=0.3 && <0.4
, monad-metrics
, mtl , mtl
, parser-combinators , parser-combinators
, persistent >=2.8 && <2.14 , persistent >=2.8 && <2.10
, persistent-sqlite >=2.6.2 , persistent-sqlite >=2.6.2
, persistent-template >=2.5 && <2.13 , persistent-template >=2.5 && <2.9
, pretty-show , pretty-show
, safe , safe
, shakespeare ==2.0.* , shakespeare >=2.0 && <2.1
, template-haskell , template-haskell
, text >=0.11 && <2.0 , text >=0.11 && <2.0
, time , time
@ -545,15 +397,16 @@ test-suite test
, unordered-containers , unordered-containers
, vector , vector
, wai , wai
, wai-extra >=3.0 && <3.2 , wai-extra >=3.0 && <3.1
, wai-logger , wai-logger >=2.2 && <2.4
, warp >=3.0 && <3.4 , wai-middleware-metrics
, warp >=3.0 && <3.3
, yaml >=0.8 && <0.12 , yaml >=0.8 && <0.12
, yesod >=1.6 && <1.8 , yesod >=1.6 && <1.7
, yesod-auth >=1.6 && <1.8 , yesod-auth >=1.6 && <1.7
, yesod-core >=1.6 && <1.8 , yesod-core >=1.6 && <1.7
, yesod-form >=1.6 && <1.8 , yesod-form >=1.6 && <1.7
, yesod-newsfeed >=1.6 && <1.8 , yesod-newsfeed >=1.6 && <1.7
, yesod-static >=1.6 && <1.8 , yesod-static >=1.6 && <1.7
, yesod-test , yesod-test
default-language: Haskell2010 default-language: Haskell2010

View file

@ -1,11 +0,0 @@
{
"folders": [
{
"path": "."
},
{
"path": "purs"
}
],
"settings": {}
}

View file

@ -1,6 +1,6 @@
name: espial name: espial
synopsis: Espial is an open-source, web-based bookmarking server. synopsis: Espial is an open-source, web-based bookmarking server.
version: "0.0.15" version: "0.0.8"
description: ! ' description: ! '
Espial is an open-source, web-based bookmarking server. Espial is an open-source, web-based bookmarking server.
@ -42,13 +42,11 @@ extra-source-files:
default-extensions: default-extensions:
- BangPatterns - BangPatterns
- BlockArguments
- CPP - CPP
- ConstraintKinds - ConstraintKinds
- DataKinds - DataKinds
- DeriveDataTypeable - DeriveDataTypeable
- DeriveGeneric - DeriveGeneric
- DerivingStrategies
- EmptyDataDecls - EmptyDataDecls
- FlexibleContexts - FlexibleContexts
- FlexibleInstances - FlexibleInstances
@ -63,7 +61,6 @@ default-extensions:
- OverloadedStrings - OverloadedStrings
- PolyKinds - PolyKinds
- PolymorphicComponents - PolymorphicComponents
- PartialTypeSignatures
- QuasiQuotes - QuasiQuotes
- Rank2Types - Rank2Types
- RankNTypes - RankNTypes
@ -76,7 +73,6 @@ default-extensions:
- TypeFamilies - TypeFamilies
- TypeOperators - TypeOperators
- TypeSynonymInstances - TypeSynonymInstances
- UndecidableInstances
- ViewPatterns - ViewPatterns
dependencies: dependencies:
@ -85,34 +81,37 @@ dependencies:
# See: https://ghc.haskell.org/trac/ghc/ticket/12130 # See: https://ghc.haskell.org/trac/ghc/ticket/12130
- base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5 - base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
- yesod >=1.6 && <1.8 - yesod >=1.6 && <1.7
- yesod-core >=1.6 && <1.8 - yesod-core >=1.6 && <1.7
- yesod-auth >=1.6 && <1.8 - yesod-auth >=1.6 && <1.7
- yesod-static >=1.6 && <1.8 - yesod-static >=1.6 && <1.7
- yesod-form >=1.6 && <1.8 - yesod-form >=1.6 && <1.7
- yesod-newsfeed >= 1.6 && < 1.8 - yesod-newsfeed >= 1.6 && < 1.7
- classy-prelude >=1.4 && <1.6 - classy-prelude >=1.4 && <1.6
- classy-prelude-conduit >=1.4 && <1.6 - classy-prelude-conduit >=1.4 && <1.6
- classy-prelude-yesod >=1.4 && <1.6 - classy-prelude-yesod >=1.4 && <1.6
- bytestring >=0.9 && <0.14 - bytestring >=0.9 && <0.11
- text >=0.11 && <2.0 - text >=0.11 && <2.0
- persistent >=2.8 && <2.14 - persistent >=2.8 && <2.10
# - persistent-postgresql >=2.8 && <2.9
- blaze-html >= 0.9 && < 1.0 - blaze-html >= 0.9 && < 1.0
- persistent-template >=2.5 && <2.13 - persistent-template >=2.5 && <2.9
- template-haskell - template-haskell
- shakespeare >=2.0 && <2.1 - shakespeare >=2.0 && <2.1
- hjsmin >=0.1 && <0.3 - hjsmin >=0.1 && <0.3
- wai-extra >=3.0 && <3.2 # - monad-control >=0.3 && <1.1
- wai-extra >=3.0 && <3.1
- yaml >=0.8 && <0.12 - yaml >=0.8 && <0.12
- http-client-tls >=0.3 && <0.4 - http-client-tls >=0.3 && <0.4
- http-conduit >=2.3 && <2.4 - http-conduit >=2.3 && <2.4
- directory >=1.1 && <1.4 - directory >=1.1 && <1.4
- warp >=3.0 && <3.4 - warp >=3.0 && <3.3
- data-default - data-default
# - aeson >=0.6 && <1.4
- conduit >=1.0 && <2.0 - conduit >=1.0 && <2.0
- monad-logger >=0.3 && <0.4 - monad-logger >=0.3 && <0.4
- fast-logger >=2.2 && <4 - fast-logger >=2.2 && <2.5
- wai-logger - wai-logger >=2.2 && <2.4
- file-embed - file-embed
- safe - safe
- unordered-containers - unordered-containers
@ -127,6 +126,8 @@ dependencies:
- attoparsec - attoparsec
- bcrypt >= 0.0.8 - bcrypt >= 0.0.8
- entropy - entropy
- ekg
- ekg-core
- esqueleto - esqueleto
- hscolour - hscolour
- http-api-data >= 0.3.4 - http-api-data >= 0.3.4
@ -134,15 +135,13 @@ dependencies:
- http-types - http-types
- iso8601-time >=0.1.3 - iso8601-time >=0.1.3
- microlens - microlens
- monad-metrics
- mtl - mtl
- persistent-sqlite >=2.6.2 - persistent-sqlite >=2.6.2
- pretty-show - pretty-show
- transformers >= 0.2.2 - transformers >= 0.2.2
- wai-middleware-metrics
- parser-combinators - parser-combinators
- html-entities
- connection
- base64
- cryptohash-sha256
# The library contains all of our application code. The executable # The library contains all of our application code. The executable
# defined below is just a thin wrapper. # defined below is just a thin wrapper.
@ -187,7 +186,6 @@ executables:
dependencies: dependencies:
- espial - espial
- optparse-generic >= 1.2.3 - optparse-generic >= 1.2.3
- optparse-applicative
# Test suite # Test suite
tests: tests:

View file

@ -1,32 +1,30 @@
.PHONY: clean build .PHONY: clean build
all: bundle all: build
install: install:
@npm run spago install spago install
build: build:
@npm run spago build @spago build
@spago bundle-app --to dist/bundle.js
bundle: build @npm run parcel-build
@npm run esbuild-bundle
@npm run esbuild-bundle-min
@rm -f dist/*.gz @rm -f dist/*.gz
@gzip -k dist/app.min.js.map @gzip -k dist/app.min.js.map
@gzip -k dist/app.min.js @gzip -k dist/app.min.js
@find dist -type f -printf "%kK\\t%h/%f\\n" | sort -k 2 @find dist -type f -printf "%kK\\t%h/%f\\n" | sort -k 2
@cp dist/app.min.js ../static/js/app.min.js
@cp dist/app.min.js.gz ../static/js/app.min.js.gz
@cp dist/app.min.js.map ../static/js/app.min.js.map @cp dist/app.min.js.map ../static/js/app.min.js.map
@cp dist/app.min.js.map.gz ../static/js/app.min.js.map.gz @cp dist/app.min.js.map.gz ../static/js/app.min.js.map.gz
@cp dist/app.min.js ../static/js/app.min.js
@cp dist/app.min.js.gz ../static/js/app.min.js.gz
docs: docs:
@rm -Rf generated-docs @rm -Rf generated-docs
@npm run purs -- docs ".spago/*/*/src/**/*.purs" --format html @purs docs ".spago/*/*/src/**/*.purs" --format html
docset: docs docset: docs
@(cd docset; python3 ./gen-docset.py) @(cd docset; python3 ./gen-docset.py)
clean: clean:
rm -f dist/* rm -f dist/*
# inotifywait -m -r -q -e close_write --format '%T %w%f' --timefmt '%T' src | while read FILE; do echo $FILE; make build; done # inotifywait -m -r -q -e close_write --format '%T %w%f' --timefmt '%T' src | while read FILE; do echo $FILE; make; done

View file

@ -1,6 +1,6 @@
## Development (Posix only) ## Development (Posix only)
1. Install `purescript`, `spago`, `parcel-bundler`: 1. Install `purescript`, `purescript-spago`, `parcel-bundler`:
``` ```
npm install npm install

View file

@ -12,7 +12,7 @@ from html import unescape
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
class Generator: class Generator:
GENERATED_DOCS = '../generated-docs/html' GENERATED_DOCS = '../generated-docs'
OUTPUT = 'purescript-local.docset' OUTPUT = 'purescript-local.docset'
def documents_path(self, *paths): def documents_path(self, *paths):

10724
purs/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,21 +2,15 @@
"name": "espial", "name": "espial",
"private": true, "private": true,
"scripts": { "scripts": {
"spago": "spago",
"purs": "purs",
"make-install": "make install", "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-watch": "inotifywait -m -r -q -e close_write --format '%T %w%f' --timefmt '%T' src | while read FILE; do echo $FILE; make; done",
"esbuild-bundle": "esbuild ./output/Main/index.js --bundle --format=iife --global-name=PS --target=chrome58,firefox57,edge18 --outfile=dist/app.js", "parcel-build": "parcel build dist/bundle.js --out-file dist/app.min.js --public-url /static/js/"
"esbuild-bundle-min": "esbuild ./output/Main/index.js --bundle --format=iife --global-name=PS --target=chrome58,firefox57,edge18 --minify --sourcemap --outfile=dist/app.min.js"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "^0.15.12", "marked": "^0.7.0",
"purescript": "^0.15.6", "moment": "^2.24.0",
"spago": "0.20.9" "parcel-bundler": "^1.12.3",
"terser": "^4.0.0"
}, },
"dependencies": { "dependencies": {}
"dompurify": "^2.4.0",
"marked": "^4.1.1",
"moment": "^2.29.4"
}
} }

View file

@ -1,42 +1,11 @@
{- let mkPackage =
### Overriding/Patching a package https://raw.githubusercontent.com/purescript/package-sets/psc-0.13.3-20190818/src/mkPackage.dhall sha256:0b197efa1d397ace6eb46b243ff2d73a3da5638d8d0ac8473e8e4a8fc528cf57
let upstream = --
in upstream
with halogen.version = "master"
with halogen.repo = "https://example.com/path/to/git/repo.git"
with halogen-vdom.version = "v4.0.0"
### Additions
let upstream = --
in upstream
with new-package-name =
{ dependencies =
[ "dependency1"
, "dependency2"
]
, repo =
"https://example.com/path/to/git/repo.git"
, version =
"<version>"
}
-}
let upstream = let upstream =
https://raw.githubusercontent.com/purescript/package-sets/psc-0.15.4-20221026/src/packages.dhall https://raw.githubusercontent.com/purescript/package-sets/psc-0.13.3-20190818/src/packages.dhall sha256:c95c4a8b8033a48a350106b759179f68a695c7ea2208228c522866fd43814dc8
sha256:8dc0b394f5861bb0136f652f3f826a88eaffb2bc0ecf0251468ed668102f5d0c
in upstream let overrides = {=}
with simple-json =
{ dependencies = let additions = {=}
[ "arrays"
, "exceptions" in upstream // overrides // additions
, "foreign"
, "foreign-object"
, "nullable"
, "prelude"
, "record"
, "typelevel-prelude"
, "variant"
]
, repo = "https://github.com/justinwoo/purescript-simple-json.git"
, version = "v9.0.0"
}

View file

@ -1,41 +1,26 @@
{ sources = [ "src/**/*.purs", "test/**/*.purs" ] { sources =
, name = "espial" [ "src/**/*.purs", "test/**/*.purs" ]
, name =
"espial"
, dependencies = , dependencies =
[ "aff" [ "aff"
, "affjax" , "simple-json"
, "affjax-web" , "affjax"
, "argonaut" , "argonaut"
, "arrays" , "arrays"
, "console" , "console"
, "const" , "debug"
, "dom-indexed" , "effect"
, "effect" , "either"
, "either" , "functions"
, "foldable-traversable" , "halogen"
, "foreign" , "prelude"
, "foreign-object" , "psci-support"
, "form-urlencoded" , "strings"
, "functions" , "transformers"
, "halogen" , "web-html"
, "http-methods" , "profunctor-lenses"
, "integers" ]
, "js-uri" , packages =
, "maybe" ./packages.dhall
, "media-types"
, "newtype"
, "nullable"
, "numbers"
, "partial"
, "prelude"
, "profunctor-lenses"
, "simple-json"
, "strings"
, "transformers"
, "tuples"
, "web-dom"
, "web-events"
, "web-html"
, "web-xhr"
]
, packages = ./packages.dhall
} }

View file

@ -2,16 +2,15 @@ module App where
import Prelude import Prelude
import Affjax.Web (Response, Error) import Affjax (Response, ResponseFormatError)
import Affjax.Web (defaultRequest) as AX import Affjax (defaultRequest) as AX
import Affjax.Web as Ax import Affjax as Ax
import Affjax.RequestBody as AXReq import Affjax.RequestBody as AXReq
import Affjax.RequestHeader (RequestHeader(..)) import Affjax.RequestHeader (RequestHeader(..))
import Affjax.ResponseFormat as AXRes import Affjax.ResponseFormat as AXRes
import Affjax.StatusCode (StatusCode(..)) import Data.Argonaut (Json)
import Data.Argonaut (decodeJson)
import Data.Array ((:)) import Data.Array ((:))
import Data.Either (Either(..), hush) import Data.Either (Either(..))
import Data.FormURLEncoded (FormURLEncoded) import Data.FormURLEncoded (FormURLEncoded)
import Data.HTTP.Method (Method(..)) import Data.HTTP.Method (Method(..))
import Data.Maybe (Maybe(..)) import Data.Maybe (Maybe(..))
@ -19,7 +18,7 @@ import Data.MediaType.Common (applicationFormURLEncoded, applicationJSON)
import Effect.Aff (Aff) import Effect.Aff (Aff)
import Effect.Class (liftEffect) import Effect.Class (liftEffect)
import Globals (app') import Globals (app')
import Model (Bookmark, Bookmark'(..), Note, Note'(..), AccountSettings, AccountSettings'(..), TagCloudMode, TagCloudMode'(..), TagCloud) import Model (Bookmark, Bookmark'(..), Note, Note'(..), AccountSettings, AccountSettings'(..))
import Simple.JSON as J import Simple.JSON as J
import Web.HTML (window) import Web.HTML (window)
import Web.HTML.Location (reload) import Web.HTML.Location (reload)
@ -35,46 +34,28 @@ toggleStar bid action = do
let path = "bm/" <> show bid <> "/" <> show action let path = "bm/" <> show bid <> "/" <> show action
void (fetchUrlEnc POST path Nothing AXRes.ignore) void (fetchUrlEnc POST path Nothing AXRes.ignore)
destroy :: Int -> Aff (Either Error (Response Unit)) destroy :: Int -> Aff (Response (Either ResponseFormatError Unit))
destroy bid = destroy bid =
fetchUrlEnc DELETE ("bm/" <> show bid) Nothing AXRes.ignore fetchUrlEnc DELETE ("bm/" <> show bid) Nothing AXRes.ignore
markRead :: Int -> Aff (Either Error (Response Unit)) markRead :: Int -> Aff (Response (Either ResponseFormatError Unit))
markRead bid = do markRead bid = do
let path = "bm/" <> show bid <> "/read" let path = "bm/" <> show bid <> "/read"
fetchUrlEnc POST path Nothing AXRes.ignore fetchUrlEnc POST path Nothing AXRes.ignore
editBookmark :: Bookmark -> Aff (Either Error (Response String)) editBookmark :: Bookmark -> Aff (Response (Either ResponseFormatError Unit))
editBookmark bm = do editBookmark bm = do
fetchJson POST "api/add" (Just (Bookmark' bm)) AXRes.string fetchJson POST "api/add" (Just (Bookmark' bm)) AXRes.ignore
editNote :: Note -> Aff (Either Error (Response String)) editNote :: Note -> Aff (Response (Either ResponseFormatError Json))
editNote bm = do editNote bm = do
fetchJson POST "api/note/add" (Just (Note' bm)) AXRes.string fetchJson POST "api/note/add" (Just (Note' bm)) AXRes.json
lookupTitle :: Bookmark -> Aff (Maybe String) destroyNote :: Int -> Aff (Response (Either ResponseFormatError Unit))
lookupTitle bm = do
eres <- fetchJson POST "api/lookuptitle" (Just (Bookmark' bm)) AXRes.string
pure $ hush eres >>= \res ->
if (res.status == StatusCode 200)
then Just res.body
else Nothing
getTagCloud :: TagCloudMode -> Aff (Maybe TagCloud)
getTagCloud mode = do
eres <- fetchJson POST "api/tagcloud" (Just (TagCloudMode' mode)) AXRes.json
pure $ hush eres >>= \res ->
hush (decodeJson res.body)
updateTagCloudMode :: TagCloudMode -> Aff (Either Error (Response Unit))
updateTagCloudMode mode = do
fetchJson POST "api/tagcloudmode" (Just (TagCloudMode' mode)) AXRes.ignore
destroyNote :: Int -> Aff (Either Error (Response Unit))
destroyNote nid = do destroyNote nid = do
fetchUrlEnc DELETE ("api/note/" <> show nid) Nothing AXRes.ignore fetchUrlEnc DELETE ("api/note/" <> show nid) Nothing AXRes.ignore
editAccountSettings :: AccountSettings -> Aff (Either Error (Response Unit)) editAccountSettings :: AccountSettings -> Aff (Response (Either ResponseFormatError Unit))
editAccountSettings us = do editAccountSettings us = do
fetchJson POST "api/accountSettings" (Just (AccountSettings' us)) AXRes.ignore fetchJson POST "api/accountSettings" (Just (AccountSettings' us)) AXRes.ignore
@ -92,7 +73,7 @@ fetchJson
-> String -> String
-> Maybe b -> Maybe b
-> AXRes.ResponseFormat a -> AXRes.ResponseFormat a
-> Aff (Either Error (Response a)) -> Aff (Response (Either ResponseFormatError a))
fetchJson method path content rt = fetchJson method path content rt =
fetchPath method path [ContentType applicationJSON] (AXReq.string <<< J.writeJSON <$> content) rt fetchPath method path [ContentType applicationJSON] (AXReq.string <<< J.writeJSON <$> content) rt
@ -102,7 +83,7 @@ fetchUrlEnc
-> String -> String
-> Maybe FormURLEncoded -> Maybe FormURLEncoded
-> AXRes.ResponseFormat a -> AXRes.ResponseFormat a
-> Aff (Either Error (Response a)) -> Aff (Response (Either ResponseFormatError a))
fetchUrlEnc method path content rt = fetchUrlEnc method path content rt =
fetchPath method path [ContentType applicationFormURLEncoded] (AXReq.FormURLEncoded <$> content) rt fetchPath method path [ContentType applicationFormURLEncoded] (AXReq.FormURLEncoded <$> content) rt
@ -113,7 +94,7 @@ fetchPath
-> Array RequestHeader -> Array RequestHeader
-> Maybe AXReq.RequestBody -> Maybe AXReq.RequestBody
-> AXRes.ResponseFormat a -> AXRes.ResponseFormat a
-> Aff (Either Error (Response a)) -> Aff (Response (Either ResponseFormatError a))
fetchPath method path headers content rt = fetchPath method path headers content rt =
fetchUrl method ((app' unit).homeR <> path) headers content rt fetchUrl method ((app' unit).homeR <> path) headers content rt
@ -124,7 +105,7 @@ fetchUrl
-> Array RequestHeader -> Array RequestHeader
-> Maybe AXReq.RequestBody -> Maybe AXReq.RequestBody
-> AXRes.ResponseFormat a -> AXRes.ResponseFormat a
-> Aff (Either Error (Response a)) -> Aff (Response (Either ResponseFormatError a))
fetchUrl method url headers content rt = fetchUrl method url headers content rt =
Ax.request Ax.request
AX.defaultRequest AX.defaultRequest

View file

@ -4,12 +4,14 @@ import Prelude hiding (div)
import App (editAccountSettings) import App (editAccountSettings)
import Data.Lens (Lens', lens, use, (%=)) import Data.Lens (Lens', lens, use, (%=))
import Data.Maybe (Maybe(..))
import Effect.Aff (Aff) import Effect.Aff (Aff)
import Globals (app')
import Halogen as H import Halogen as H
import Halogen.HTML (div, input, text) import Halogen.HTML (HTML, div, input, text)
import Halogen.HTML.Elements (label) import Halogen.HTML.Elements (label)
import Halogen.HTML.Events (onChecked) import Halogen.HTML.Events (onChecked)
import Halogen.HTML.Properties (InputType(..), checked, for, id, name, type_) import Halogen.HTML.Properties (InputType(..), checked, for, id_, name, type_)
import Model (AccountSettings) import Model (AccountSettings)
import Util (class_) import Util (class_)
import Web.Event.Event (Event) import Web.Event.Event (Event)
@ -32,7 +34,7 @@ data EditField
-- | The bookmark component definition. -- | The bookmark component definition.
usetting :: forall q i o. AccountSettings -> H.Component q i o Aff usetting :: forall q i o. AccountSettings -> H.Component HTML q i o Aff
usetting u' = usetting u' =
H.mkComponent H.mkComponent
{ initialState: const (mkState u') { initialState: const (mkState u')
@ -40,6 +42,7 @@ usetting u' =
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction } , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
} }
where where
app = app' unit
mkState u = mkState u =
{ us: u { us: u
@ -50,27 +53,27 @@ usetting u' =
div [ class_ "settings-form" ] div [ class_ "settings-form" ]
[ div [ class_ "fw7 mb2"] [ text "Account Settings" ] [ div [ class_ "fw7 mb2"] [ text "Account Settings" ]
, div [ class_ "flex items-center mb2" ] , div [ class_ "flex items-center mb2" ]
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id "archiveDefault", name "archiveDefault" [ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "archiveDefault", name "archiveDefault"
, checked (us.archiveDefault) , onChecked (editField EarchiveDefault) ] , checked (us.archiveDefault) , onChecked (editField EarchiveDefault) ]
, label [ for "archiveDefault", class_ "lh-copy" ] , label [ for "archiveDefault", class_ "lh-copy" ]
[ text "Archive Non-Private Bookmarks (archive.li)" ] [ text "Archive Non-Private Bookmarks (archive.li)" ]
] ]
, div [ class_ "flex items-center mb2" ] , div [ class_ "flex items-center mb2" ]
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id "privateDefault", name "privateDefault" [ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "privateDefault", name "privateDefault"
, checked (us.privateDefault) , onChecked (editField EprivateDefault) ] , checked (us.privateDefault) , onChecked (editField EprivateDefault) ]
, label [ for "privateDefault", class_ "lh-copy" ] , label [ for "privateDefault", class_ "lh-copy" ]
[ text "Default new bookmarks to Private" ] [ text "Default new bookmarks to Private" ]
] ]
, div [ class_ "flex items-center mb2" ] , div [ class_ "flex items-center mb2" ]
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id "privacyLock", name "privacyLock" [ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "privacyLock", name "privacyLock"
, checked (us.privacyLock) , onChecked (editField EprivacyLock) ] , checked (us.privacyLock) , onChecked (editField EprivacyLock) ]
, label [ for "privacyLock", class_ "lh-copy" ] , label [ for "privacyLock", class_ "lh-copy" ]
[ text "Privacy Lock (Private Account)" ] [ text "Privacy Lock (Private Account)" ]
] ]
] ]
where where
editField :: forall a. (a -> EditField) -> a -> UAction editField :: forall a. (a -> EditField) -> a -> Maybe UAction
editField f = UEditField <<< f editField f = Just <<< UEditField <<< f
handleAction :: UAction -> H.HalogenM UState UAction () o Aff Unit handleAction :: UAction -> H.HalogenM UState UAction () o Aff Unit
handleAction (UEditField f) = do handleAction (UEditField f) = do
@ -81,6 +84,6 @@ usetting u' =
us <- use _us us <- use _us
void $ H.liftAff (editAccountSettings us) void $ H.liftAff (editAccountSettings us)
handleAction (USubmit _) = do handleAction (USubmit e) = do
us <- use _us us <- use _us
void $ H.liftAff (editAccountSettings us) void $ H.liftAff (editAccountSettings us)

View file

@ -2,36 +2,32 @@ module Component.Add where
import Prelude hiding (div) import Prelude hiding (div)
import Affjax (printError) import App (destroy, editBookmark)
import Affjax.StatusCode (StatusCode(..)) import Data.Array (drop, foldMap)
import App (destroy, editBookmark, lookupTitle)
import Data.Either (Either(..))
import Data.Lens (Lens', lens, use, (%=), (.=)) import Data.Lens (Lens', lens, use, (%=), (.=))
import Data.Maybe (Maybe(..), fromMaybe, isJust, maybe) import Data.Maybe (Maybe(..), maybe)
import Data.Monoid (guard) import Data.Monoid (guard)
import Data.String (Pattern(..), null, stripPrefix) import Data.String (null)
import Data.String (split) as S
import Data.String.Pattern (Pattern(..))
import Data.Tuple (fst, snd) import Data.Tuple (fst, snd)
import Effect.Aff (Aff) import Effect.Aff (Aff)
import Effect.Class (liftEffect) import Effect.Class (liftEffect)
import Effect.Console (log) import Globals (app', closeWindow, mmoment8601)
import Globals (closeWindow, mmoment8601)
import Halogen as H import Halogen as H
import Halogen.HTML (button, div, form, input, label, p, span, table, tbody_, td, td_, text, textarea, tr_) import Halogen.HTML (HTML, br_, button, 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 (onSubmit, onValueChange, onChecked, onClick)
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, autofocus, checked, disabled, for, id, name, required, rows, title, type_, value) import Halogen.HTML.Properties (autofocus, ButtonType(..), InputType(..), autocomplete, checked, for, id_, name, required, rows, title, type_, value)
import Model (Bookmark) import Model (Bookmark)
import Util (_curQuerystring, _loc, _doc, _lookupQueryStringValue, attr, class_, ifElseH, whenH) import Util (_curQuerystring, _loc, _lookupQueryStringValue, attr, class_, ifElseH, whenH)
import Web.Event.Event (Event, preventDefault) import Web.Event.Event (Event, preventDefault)
import Web.HTML (window) import Web.HTML (window)
import Web.HTML.HTMLDocument (referrer) import Web.HTML.Location (setHref)
import Web.HTML.Location (setHref, origin)
import DOM.HTML.Indexed.AutocompleteType (AutocompleteType(..))
data BAction data BAction
= BEditField EditField = BEditField EditField
| BEditSubmit Event | BEditSubmit Event
| BDeleteAsk Boolean | BDeleteAsk Boolean
| BLookupTitle
| BDestroy | BDestroy
data EditField data EditField
@ -46,9 +42,7 @@ type BState =
{ bm :: Bookmark { bm :: Bookmark
, edit_bm :: Bookmark , edit_bm :: Bookmark
, deleteAsk :: Boolean , deleteAsk :: Boolean
, loading :: Boolean
, destroyed :: Boolean , destroyed :: Boolean
, apiError :: Maybe String
} }
_bm :: Lens' BState Bookmark _bm :: Lens' BState Bookmark
@ -57,10 +51,7 @@ _bm = lens _.bm (_ { bm = _ })
_edit_bm :: Lens' BState Bookmark _edit_bm :: Lens' BState Bookmark
_edit_bm = lens _.edit_bm (_ { edit_bm = _ }) _edit_bm = lens _.edit_bm (_ { edit_bm = _ })
_apiError :: Lens' BState (Maybe String) addbmark :: forall q i o. Bookmark -> H.Component HTML q i o Aff
_apiError = lens _.apiError (_ { apiError = _ })
addbmark :: forall q i o. Bookmark -> H.Component q i o Aff
addbmark b' = addbmark b' =
H.mkComponent H.mkComponent
{ initialState: const (mkState b') { initialState: const (mkState b')
@ -68,64 +59,59 @@ addbmark b' =
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction } , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
} }
where where
app = app' unit
mkState b = mkState b =
{ bm: b { bm: b
, edit_bm: b , edit_bm: b
, deleteAsk: false , deleteAsk: false
, destroyed: false , destroyed: false
, loading: false
, apiError: Nothing
} }
render :: forall m. BState -> H.ComponentHTML BAction () m render :: forall m. BState -> H.ComponentHTML BAction () m
render s@{ bm, edit_bm, apiError } = render s@{ bm, edit_bm } =
ifElseH (not s.destroyed) ifElseH (not s.destroyed)
display_edit display_edit
display_destroyed display_destroyed
where where
display_edit _ = display_edit _ =
form [ onSubmit BEditSubmit ] form [ onSubmit (Just <<< BEditSubmit) ]
[ table [ class_ "w-100" ] [ table [ class_ "w-100" ]
[ tbody_ [ tbody_
[ tr_ [ tr_
[ td [ class_ "w1" ] [ ] [ td [ class_ "w1" ] [ ]
, td_ [ whenH (bm.bid > 0) , td_ [ whenH (bm.bid > 0)
display_exists, display_exists
whenH (isJust apiError)
(alert_notification (fromMaybe "" apiError))
] ]
] ]
, tr_ , tr_
[ td_ [ label [ for "url" ] [ text "URL" ] ] [ td_ [ label [ for "url" ] [ text "URL" ] ]
, td_ [ input [ type_ InputUrl , id "url", class_ "w-100 mv1" , required true, name "url", autofocus (null bm.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)] ] , value (edit_bm.url) , onValueChange (editField Eurl)] ]
] ]
, tr_ , tr_
[ td_ [ label [ for "title" ] [ text "title" ] ] [ td_ [ label [ for "title" ] [ text "title" ] ]
, td [class_ "flex"] , td_ [ input [ type_ InputText , id_ "title", class_ "w-100 mv1" , name "title"
[ input [ type_ InputText , id "title", class_ "w-100 mv1 flex-auto" , name "title" , value (edit_bm.title) , onValueChange (editField Etitle)] , value (edit_bm.title) , onValueChange (editField Etitle)] ]
, button [ disabled s.loading, type_ ButtonButton, onClick \_ -> BLookupTitle, class_ ("ml2 input-reset ba b--navy pointer f6 di dim pa1 ma1 mr0 " <> guard s.loading "bg-light-silver") ] [ text "fetch" ]
]
] ]
, tr_ , tr_
[ td_ [ label [ for "description" ] [ text "description" ] ] [ td_ [ label [ for "description" ] [ text "description" ] ]
, td_ [ textarea [ class_ "w-100 mt1 mid-gray" , id "description", name "description", rows 4 , td_ [ textarea [ class_ "w-100 mt1 mid-gray" , id_ "description", name "description", rows 4
, value (edit_bm.description) , onValueChange (editField Edescription)] ] , value (edit_bm.description) , onValueChange (editField Edescription)] ]
] ]
, tr_ , tr_
[ td_ [ label [ for "tags" ] [ text "tags" ] ] [ td_ [ label [ for "tags" ] [ text "tags" ] ]
, td_ [ input [ type_ InputText , id "tags", class_ "w-100 mv1" , name "tags", autocomplete AutocompleteOff, attr "autocapitalize" "off", autofocus (not $ null bm.url) , 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)] ] , value (edit_bm.tags) , onValueChange (editField Etags)] ]
] ]
, tr_ , tr_
[ td_ [ label [ for "private" ] [ text "private" ] ] [ td_ [ label [ for "private" ] [ text "private" ] ]
, td_ [ input [ type_ InputCheckbox , id "private", class_ "private pointer" , name "private" , td_ [ input [ type_ InputCheckbox , id_ "private", class_ "private pointer" , name "private"
, checked (edit_bm.private) , onChecked (editField Eprivate)] ] , checked (edit_bm.private) , onChecked (editField Eprivate)] ]
] ]
, tr_ , tr_
[ td_ [ label [ for "toread" ] [ text "read later" ] ] [ td_ [ label [ for "toread" ] [ text "read later" ] ]
, td_ [ input [ type_ InputCheckbox , id "toread", class_ "toread pointer" , name "toread" , td_ [ input [ type_ InputCheckbox , id_ "toread", class_ "toread pointer" , name "toread"
, checked (edit_bm.toread) , onChecked (editField Etoread)] ] , checked (edit_bm.toread) , onChecked (editField Etoread)] ]
] ]
, tr_ , tr_
@ -144,46 +130,32 @@ addbmark b' =
[ text (maybe " " fst mmoment) ] [ text (maybe " " fst mmoment) ]
, div [ class_ "edit_links dib ml1" ] , div [ class_ "edit_links dib ml1" ]
[ div [ class_ "delete_link di" ] [ div [ class_ "delete_link di" ]
[ button ([ type_ ButtonButton, onClick \_ -> BDeleteAsk true, class_ "delete" ] <> guard s.deleteAsk [ attr "hidden" "hidden" ]) [ text "delete" ] [ button ([ type_ ButtonButton, onClick \_ -> Just (BDeleteAsk true), class_ "delete" ] <> guard s.deleteAsk [ attr "hidden" "hidden" ]) [ text "delete" ]
, span ([ class_ "confirm red" ] <> guard (not s.deleteAsk) [ attr "hidden" "hidden" ]) , span ([ class_ "confirm red" ] <> guard (not s.deleteAsk) [ attr "hidden" "hidden" ])
[ button [ type_ ButtonButton, onClick \_ -> BDeleteAsk false] [ text "cancel / " ] [ button [ type_ ButtonButton, onClick \_ -> Just (BDeleteAsk false)] [ text "cancel / " ]
, button [ type_ ButtonButton, onClick \_ -> BDestroy, class_ "red" ] [ text "destroy" ] , button [ type_ ButtonButton, onClick \_ -> Just BDestroy, class_ "red" ] [ text "destroy" ]
] ]
] ]
] ]
] ]
alert_notification alert_text _ =
div [ class_ "alert alert-err" ] [ text alert_text ]
display_destroyed _ = p [ class_ "red"] [text "you killed this bookmark"] display_destroyed _ = p [ class_ "red"] [text "you killed this bookmark"]
editField :: forall a. (a -> EditField) -> a -> BAction editField :: forall a. (a -> EditField) -> a -> Maybe BAction
editField f = BEditField <<< f editField f = Just <<< BEditField <<< f
mmoment = mmoment8601 bm.time mmoment = mmoment8601 bm.time
-- toTextarea = toTextarea =
-- drop 1 drop 1
-- <<< foldMap (\x -> [br_, text x]) <<< foldMap (\x -> [br_, text x])
-- <<< S.split (Pattern "\n") <<< S.split (Pattern "\n")
handleAction :: BAction -> H.HalogenM BState BAction () o Aff Unit handleAction :: BAction -> H.HalogenM BState BAction () o Aff Unit
handleAction (BDeleteAsk e) = do handleAction (BDeleteAsk e) = do
H.modify_ (_ { deleteAsk = e }) H.modify_ (_ { deleteAsk = e })
handleAction BLookupTitle = do
H.modify_ (_ { loading = true })
edit_bm <- H.gets _.edit_bm
mtitle <- H.liftAff $ lookupTitle edit_bm
case mtitle of
Just title' -> _edit_bm %= (_ { title = title' })
Nothing -> pure $ unit
H.modify_ (_ { loading = false })
handleAction (BDestroy) = do handleAction (BDestroy) = do
bid <- H.gets _.bm.bid bid <- H.gets _.bm.bid
void $ H.liftAff (destroy bid) void $ H.liftAff (destroy bid)
H.modify_ (_ { destroyed = true }) H.modify_ (_ { destroyed = true })
handleAction (BEditField f) = do handleAction (BEditField f) = do
_edit_bm %= case f of _edit_bm %= case f of
Eurl e -> _ { url = e } Eurl e -> _ { url = e }
@ -192,29 +164,14 @@ addbmark b' =
Etags e -> _ { tags = e } Etags e -> _ { tags = e }
Eprivate e -> _ { private = e } Eprivate e -> _ { private = e }
Etoread e -> _ { toread = e } Etoread e -> _ { toread = e }
handleAction (BEditSubmit e) = do handleAction (BEditSubmit e) = do
liftEffect (preventDefault e) H.liftEffect (preventDefault e)
edit_bm <- use _edit_bm edit_bm <- use _edit_bm
_apiError .= Nothing void $ H.liftAff (editBookmark edit_bm)
H.liftAff (editBookmark edit_bm) >>= case _ of _bm .= edit_bm
Left affErr -> do loc <- liftEffect _loc
_apiError .= Just (printError affErr) win <- liftEffect window
liftEffect $ log (printError affErr) qs <- liftEffect _curQuerystring
Right { status: StatusCode s } | s >= 200 && s < 300 -> do case _lookupQueryStringValue qs "next" of
_bm .= edit_bm Just n -> liftEffect (setHref n loc)
qs <- liftEffect $ _curQuerystring _ -> liftEffect (closeWindow win)
doc <- liftEffect $ _doc
ref <- liftEffect $ referrer doc
loc <- liftEffect $ _loc
org <- liftEffect $ origin loc
case _lookupQueryStringValue qs "next" of
Just "closeWindow" -> liftEffect $ closeWindow =<< window
Just "back" -> liftEffect $
case stripPrefix (Pattern org) ref of
Just _ -> setHref ref loc
Nothing -> setHref org loc
_ -> liftEffect $ closeWindow =<< window
Right res -> do
_apiError .= Just (res.body)
liftEffect $ log (res.body)

View file

@ -1,38 +1,39 @@
module Component.BList where module Component.BList where
import Prelude import Prelude
import Component.BMark (BMessage(..), BSlot, bmark) import Component.BMark (BMessage(..), BSlot, bmark)
import Model (Bookmark, BookmarkId) import Model (Bookmark, BookmarkId)
import Data.Array (filter) import Data.Array (filter)
import Effect.Aff (Aff) import Data.Maybe (Maybe(..))
import Halogen as H import Effect.Aff (Aff)
import Halogen.HTML as HH import Halogen as H
import Type.Proxy (Proxy(..)) import Halogen.HTML as HH
import Data.Symbol (SProxy(..))
data LAction =
HandleBMessage BookmarkId BMessage data LAction =
HandleBMessage BookmarkId BMessage
type ChildSlots =
( bookmark :: BSlot Int type ChildSlots =
) ( bookmark :: BSlot Int
)
_bookmark = Proxy :: Proxy "bookmark"
_bookmark = SProxy :: SProxy "bookmark"
blist :: forall q i o. Array Bookmark -> H.Component q i o Aff
blist st = blist :: forall q i o. Array Bookmark -> H.Component HH.HTML q i o Aff
H.mkComponent blist st =
{ initialState: const st H.mkComponent
, render { initialState: const st
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction } , render
} , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
where }
where
render :: Array Bookmark -> H.ComponentHTML LAction ChildSlots Aff
render bms = render :: Array Bookmark -> H.ComponentHTML LAction ChildSlots Aff
HH.div_ $ map (\b -> HH.slot _bookmark b.bid (bmark b) unit (HandleBMessage b.bid)) bms render bms =
HH.div_ $ map (\b -> HH.slot _bookmark b.bid (bmark b) unit (Just <<< HandleBMessage b.bid)) bms
handleAction :: LAction -> H.HalogenM (Array Bookmark) LAction ChildSlots o Aff Unit
handleAction (HandleBMessage bid BNotifyRemove) = do handleAction :: LAction -> H.HalogenM (Array Bookmark) LAction ChildSlots o Aff Unit
H.modify_ (filter (\b -> b.bid /= bid)) handleAction (HandleBMessage bid BNotifyRemove) = do
H.modify_ (filter (\b -> b.bid /= bid))

View file

@ -1,286 +1,244 @@
module Component.BMark where module Component.BMark where
import Prelude hiding (div) import Prelude hiding (div)
import Affjax (printError) import App (StarAction(..), destroy, editBookmark, markRead, toggleStar)
import Affjax.StatusCode (StatusCode(..)) import Component.Markdown as Markdown
import App (StarAction(..), destroy, editBookmark, markRead, toggleStar, lookupTitle) import Data.Const (Const)
import Component.Markdown as Markdown import Data.Lens (Lens', lens, use, (%=), (.=))
import Data.Const (Const) import Data.Maybe (Maybe(..), fromMaybe, isJust)
import Data.Either (Either(..)) import Data.Monoid (guard)
import Data.Lens (Lens', lens, use, (%=), (.=)) import Data.Nullable (toMaybe)
import Data.Maybe (Maybe(..), fromMaybe, isJust) import Data.String (null, split, take) as S
import Data.Monoid (guard) import Data.String.Pattern (Pattern(..))
import Data.Nullable (toMaybe) import Data.Symbol (SProxy(..))
import Data.String (null, split, take, replaceAll) as S import Effect.Aff (Aff)
import Data.String.Pattern (Pattern(..), Replacement(..)) import Globals (app')
import Effect.Aff (Aff) import Halogen as H
import Effect.Class (liftEffect) import Halogen.HTML (HTML, a, br_, button, div, div_, form, input, label, span, text, textarea)
import Effect.Class.Console (log) import Halogen.HTML as HH
import Globals (app', setFocus, toLocaleDateString) import Halogen.HTML.Events (onSubmit, onValueChange, onChecked, onClick)
import Halogen as H import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, checked, for, href, id_, name, required, rows, target, title, type_, value)
import Halogen.HTML (a, br_, button, div, div_, form, input, label, span, text, textarea) import Model (Bookmark)
import Halogen.HTML as HH import Util (attr, class_, fromNullableStr, ifElseH, whenH, whenA)
import Halogen.HTML.Events (onSubmit, onValueChange, onChecked, onClick) import Web.Event.Event (Event, preventDefault)
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, checked, disabled, for, href, id, name, required, rows, target, title, type_, value)
import Model (Bookmark) -- | UI Events
import Type.Proxy (Proxy(..)) data BAction
import Util (attr, class_, encodeTag, fromNullableStr, ifElseH, whenA, whenH) = BStar Boolean
import Web.Event.Event (Event, preventDefault) | BDeleteAsk Boolean
import DOM.HTML.Indexed.AutocompleteType (AutocompleteType(..)) | BDestroy
| BEdit Boolean
-- | UI Events | BEditField EditField
data BAction | BEditSubmit Event
= BStar Boolean | BMarkRead
| BDeleteAsk Boolean
| BLookupTitle -- | FormField Edits
| BDestroy data EditField
| BEdit Boolean = Eurl String
| BEditField EditField | Etitle String
| BEditSubmit Event | Edescription String
| BMarkRead | Etags String
| Eprivate Boolean
-- | FormField Edits | Etoread Boolean
data EditField
= Eurl String -- | Messages to parent
| Etitle String data BMessage
| Edescription String = BNotifyRemove
| Etags String
| Eprivate Boolean type BSlot = H.Slot (Const Void) BMessage
| Etoread Boolean
type BState =
-- | Messages to parent { bm :: Bookmark
data BMessage , edit_bm :: Bookmark
= BNotifyRemove , deleteAsk:: Boolean
, edit :: Boolean
type BSlot = H.Slot (Const Void) BMessage }
type BState = _bm :: Lens' BState Bookmark
{ bm :: Bookmark _bm = lens _.bm (_ { bm = _ })
, edit_bm :: Bookmark
, deleteAsk:: Boolean _edit_bm :: Lens' BState Bookmark
, edit :: Boolean _edit_bm = lens _.edit_bm (_ { edit_bm = _ })
, loading :: Boolean
, apiError :: Maybe String _edit :: Lens' BState Boolean
} _edit = lens _.edit (_ { edit = _ })
_bm :: Lens' BState Bookmark _markdown = SProxy :: SProxy "markdown"
_bm = lens _.bm (_ { bm = _ })
type ChildSlots =
_edit_bm :: Lens' BState Bookmark ( markdown :: Markdown.Slot Unit
_edit_bm = lens _.edit_bm (_ { edit_bm = _ }) )
_edit :: Lens' BState Boolean bmark :: forall q i. Bookmark -> H.Component HTML q i BMessage Aff
_edit = lens _.edit (_ { edit = _ }) bmark b' =
H.mkComponent
_apiError :: Lens' BState (Maybe String) { initialState: const (mkState b')
_apiError = lens _.apiError (_ { apiError = _ }) , render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
_markdown = Proxy :: Proxy "markdown" }
where
type ChildSlots = app = app' unit
( markdown :: Markdown.Slot Unit
) mkState b =
{ bm: b
bmark :: forall q i. Bookmark -> H.Component q i BMessage Aff , edit_bm: b
bmark b' = , deleteAsk: false
H.mkComponent , edit: false
{ initialState: const (mkState b') }
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction } render :: BState -> H.ComponentHTML BAction ChildSlots Aff
} render s@{ bm, edit_bm } =
where div [ id_ (show bm.bid) , class_ ("bookmark w-100 mw7 pa1 mb3" <> guard bm.private " private")] $
app = app' unit [ whenH app.dat.isowner
star
mkState b = , ifElseH s.edit
{ bm: b display_edit
, edit_bm: b display
, deleteAsk: false ]
, edit: false
, loading: false where
, apiError: Nothing
} star _ =
div [ class_ ("star fl pointer" <> guard bm.selected " selected") ]
render :: BState -> H.ComponentHTML BAction ChildSlots Aff [ button [ class_ "moon-gray", onClick \_ -> Just (BStar (not bm.selected)) ] [ text "✭" ] ]
render s@{ bm, edit_bm, apiError } =
div [ id (show bm.bid) , class_ ("bookmark w-100 mw7 pa1 mb3" <> guard bm.private " private")] $ display _ =
[ whenH app.dat.isowner div [ class_ "display" ] $
star [ a [ href bm.url, target "_blank", class_ ("link f5 lh-title" <> guard bm.toread " unread")]
, ifElseH s.edit [ text $ if S.null bm.title then "[no title]" else bm.title ]
display_edit , br_
display , 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"))
where , target "_blank", title "archive link"]
[ if isJust (toMaybe bm.archiveUrl) then text "☑" else text "☐" ]
star _ = , br_
div [ class_ ("star fl pointer" <> guard bm.selected " selected") ] , div [ class_ "description mt1 mid-gray" ] [ HH.slot _markdown unit Markdown.component bm.description absurd ]
[ button [ class_ "moon-gray", onClick \_ -> BStar (not bm.selected) ] [ text "✭" ] ] , div [ class_ "tags" ] $
whenA (not (S.null bm.tags)) $ \_ ->
display _ = map (\tag -> a [ class_ ("link tag mr1" <> guard (S.take 1 tag == ".") " private")
div [ class_ "display" ] $ , href (linkToFilterTag tag) ]
[ a [ href bm.url, target "_blank", class_ ("link f5 lh-title" <> guard bm.toread " unread")] [ text tag ])
[ text $ if S.null bm.title then "[no title]" else bm.title ] (S.split (Pattern " ") bm.tags)
, br_
, a [ href bm.url , class_ "link f7 gray hover-blue" ] [ text bm.url ] , a [ class_ "link f7 dib gray w4", href (linkToFilterSingle bm.slug) ]
, a [ href (fromMaybe ("http://archive.is/" <> bm.url) (toMaybe bm.archiveUrl)) [ text shtime ]
, class_ ("link f7 gray hover-blue ml2" <> (guard (isJust (toMaybe bm.archiveUrl)) " green"))
, target "_blank", title "archive link"] -- links
[ if isJust (toMaybe bm.archiveUrl) then text "☑" else text "☐" ] , whenH app.dat.isowner $ \_ ->
, br_ div [ class_ "edit_links di" ]
, div [ class_ "description mt1 mid-gray" ] [ HH.slot _markdown unit Markdown.component bm.description absurd ] [ button [ type_ ButtonButton, onClick \_ -> Just (BEdit true), class_ "edit light-silver hover-blue" ] [ text "edit  " ]
, div [ class_ "tags" ] $ , div [ class_ "delete_link di" ]
whenA (not (S.null bm.tags)) $ \_ -> [ button [ type_ ButtonButton, onClick \_ -> Just (BDeleteAsk true), class_ ("delete light-silver hover-blue" <> guard s.deleteAsk " dn") ] [ text "delete" ]
map (\tag -> a [ class_ ("link tag mr1" <> guard (S.take 1 tag == ".") " private") , span ([ class_ ("confirm red" <> guard (not s.deleteAsk) " dn") ] )
, href (linkToFilterTag tag) ] [ button [ type_ ButtonButton, onClick \_ -> Just (BDeleteAsk false)] [ text "cancel / " ]
[ text tag ]) , button [ type_ ButtonButton, onClick \_ -> Just BDestroy, class_ "red" ] [ text "destroy" ]
(S.split (Pattern " ") bm.tags) ]
]
, a [ class_ "link f7 dib gray w4", href (linkToFilterSingle bm.slug), title shdatetime ] ]
[ text shdate ] , whenH app.dat.isowner $ \_ ->
div [ class_ "read di" ] $
-- links guard bm.toread
, whenH app.dat.isowner $ \_ -> [ text "  "
div [ class_ "edit_links di" ] , button [ onClick \_ -> Just BMarkRead, class_ "mark_read" ] [ text "mark as read"]
[ button [ type_ ButtonButton, onClick \_ -> BEdit true, class_ "edit light-silver hover-blue" ] [ text "edit  " ] ]
, div [ class_ "delete_link di" ] ]
[ button [ type_ ButtonButton, onClick \_ -> 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 \_ -> BDeleteAsk false] [ text "cancel / " ] display_edit _ =
, button [ type_ ButtonButton, onClick \_ -> BDestroy, class_ "red" ] [ text "destroy" ] div [ class_ "edit_bookmark_form pa2 pt0 bg-white" ] $
] [ form [ onSubmit (Just <<< BEditSubmit) ]
] [ div_ [ text "url" ]
] , input [ type_ InputUrl , class_ "url w-100 mb2 pt1 f7 edit_form_input" , required true , name "url"
, whenH app.dat.isowner $ \_ -> , value (edit_bm.url) , onValueChange (editField Eurl) ]
div [ class_ "read di" ] $ , br_
guard bm.toread , div_ [ text "title" ]
[ text "  " , input [ type_ InputText , class_ "title w-100 mb2 pt1 f7 edit_form_input" , name "title"
, button [ onClick \_ -> BMarkRead, class_ "mark_read" ] [ text "mark as read"] , 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) ]
display_edit _ = , br_
div [ class_ "edit_bookmark_form pa2 pt0 bg-white" ] $ , div [ id_ "tags_input_box"]
[ whenH (isJust apiError) [ div_ [ text "tags" ]
(alert_notification (fromMaybe "" apiError)) , input [ type_ InputText , class_ "tags w-100 mb1 pt1 f7 edit_form_input" , name "tags"
, form [ onSubmit BEditSubmit ] , autocomplete false, attr "autocapitalize" "off"
[ div_ [ text "url" ] , value (edit_bm.tags) , onValueChange (editField Etags) ]
, input [ type_ InputUrl , class_ "url w-100 mb2 pt1 edit_form_input" , required true , name "url" , br_
, value (edit_bm.url) , onValueChange (editField Eurl) ] ]
, div_ [ text "title" ] , div [ class_ "edit_form_checkboxes mv3"]
, div [class_ "flex"] [ input [ type_ InputCheckbox , class_ "private pointer" , id_ "edit_private", name "private"
[input [ type_ InputText , class_ "title w-100 mb2 pt1 edit_form_input" , name "title" , checked (edit_bm.private) , onChecked (editField Eprivate) ]
, value (edit_bm.title) , onValueChange (editField Etitle) ] , text " "
, button [ disabled s.loading, type_ ButtonButton, onClick \_ -> BLookupTitle, class_ ("ml1 pa1 mb2 dark-gray ba b--moon-gray bg-near-white pointer rdim f7 " <> guard s.loading "bg-light-silver") ] [ text "fetch" ] , label [ for "edit_private" , class_ "mr2" ] [ text "private" ]
] , text " "
, div_ [ text "description" ] , input [ type_ InputCheckbox , class_ "toread pointer" , id_ "edit_toread", name "toread"
, textarea [ class_ "description w-100 mb1 pt1 edit_form_input" , name "description", rows 5 , checked (edit_bm.toread) , onChecked (editField Etoread) ]
, value (edit_bm.description) , onValueChange (editField Edescription) ] , text " "
, div [ id "tags_input_box"] , label [ for "edit_toread" ] [ text "to-read" ]
[ div_ [ text "tags" ] , br_
, input [ id (tagid edit_bm), type_ InputText , class_ "tags w-100 mb1 pt1 edit_form_input" , name "tags" ]
, autocomplete AutocompleteOff, attr "autocapitalize" "off" , input [ type_ InputSubmit , class_ "mr1 pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "save" ]
, value (edit_bm.tags) , onValueChange (editField Etags) ] , text " "
] , input [ type_ InputReset , class_ "pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "cancel"
, div [ class_ "edit_form_checkboxes mv3"] , onClick \_ -> Just (BEdit false) ]
[ 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 " " editField :: forall a. (a -> EditField) -> a -> Maybe BAction
, input [ type_ InputCheckbox , class_ "toread pointer" , id "edit_toread", name "toread" editField f = Just <<< BEditField <<< f
, checked (edit_bm.toread) , onChecked (editField Etoread) ] linkToFilterSingle slug = fromNullableStr app.userR <> "/b:" <> slug
, text " " linkToFilterTag tag = fromNullableStr app.userR <> "/t:" <> tag
, label [ for "edit_toread" ] [ text "to-read" ] shtime = S.take 16 bm.time `append` "Z"
]
, input [ type_ InputSubmit , class_ "mr1 pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "save" ] handleAction :: BAction -> H.HalogenM BState BAction ChildSlots BMessage Aff Unit
, text " "
, input [ type_ InputReset , class_ "pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "cancel" -- | Star
, onClick \_ -> BEdit false ] handleAction (BStar e) = do
] bm <- use _bm
] H.liftAff (toggleStar bm.bid (if e then Star else UnStar))
_bm %= _ { selected = e }
alert_notification alert_text _ = _edit_bm %= _ { selected = e }
div [ class_ "alert alert-err" ] [ text alert_text ]
-- | Delete
editField :: forall a. (a -> EditField) -> a -> BAction handleAction (BDeleteAsk e) = do
editField f = BEditField <<< f H.modify_ (_ { deleteAsk = e })
linkToFilterSingle slug = fromNullableStr app.userR <> "/b:" <> slug
linkToFilterTag tag = fromNullableStr app.userR <> "/t:" <> encodeTag tag -- | Destroy
shdate = toLocaleDateString bm.time handleAction (BDestroy) = do
shdatetime = S.take 16 bm.time `append` "Z" bm <- use _bm
void $ H.liftAff (destroy bm.bid)
tagid bm = show bm.bid <> "_tags" H.raise BNotifyRemove
handleAction :: BAction -> H.HalogenM BState BAction ChildSlots BMessage Aff Unit -- | Mark Read
handleAction (BMarkRead) = do
-- | Star bm <- use _bm
handleAction (BStar e) = do void (H.liftAff (markRead bm.bid))
bm <- use _bm _bm %= _ { toread = false }
H.liftAff (toggleStar bm.bid (if e then Star else UnStar))
_bm %= _ { selected = e } -- | Start/Stop Editing
_edit_bm %= _ { selected = e } handleAction (BEdit e) = do
bm <- use _bm
-- | Delete _edit_bm .= bm
handleAction (BDeleteAsk e) = do _edit .= e
H.modify_ (_ { deleteAsk = e })
-- | Update Form Field
-- | Destroy handleAction (BEditField f) = do
handleAction (BDestroy) = do _edit_bm %= case f of
bm <- use _bm Eurl e -> _ { url = e }
void $ H.liftAff (destroy bm.bid) Etitle e -> _ { title = e }
H.raise BNotifyRemove Edescription e -> _ { description = e }
Etags e -> _ { tags = e }
-- | Mark Read Eprivate e -> _ { private = e }
handleAction (BMarkRead) = do Etoread e -> _ { toread = e }
bm <- use _bm
void (H.liftAff (markRead bm.bid)) -- | Submit
_bm %= _ { toread = false } handleAction (BEditSubmit e) = do
H.liftEffect (preventDefault e)
-- | Start/Stop Editing edit_bm <- use _edit_bm
handleAction (BEdit e) = do void $ H.liftAff (editBookmark edit_bm)
bm <- use _bm _bm .= edit_bm
_edit_bm .= bm _edit .= false
_edit .= e
_apiError .= Nothing
H.liftEffect $
when e
(setFocus (tagid bm))
-- | Update Form Field
handleAction (BEditField f) = 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 }
-- | Lookup Title
handleAction BLookupTitle = do
H.modify_ (_ { loading = true })
edit_bm <- H.gets _.edit_bm
mtitle <- H.liftAff $ lookupTitle edit_bm
case mtitle of
Just title' -> _edit_bm %= (_ { title = title' })
Nothing -> pure $ unit
H.modify_ (_ { loading = false })
-- | Submit
handleAction (BEditSubmit e) = do
H.liftEffect (preventDefault e)
edit_bm <- use _edit_bm
_apiError .= Nothing
let edit_bm' = edit_bm { tags = S.replaceAll (Pattern ",") (Replacement " ") edit_bm.tags }
H.liftAff (editBookmark edit_bm') >>= case _ of
Left affErr -> do
_apiError .= Just (printError affErr)
liftEffect $ log (printError affErr)
Right { status: StatusCode s } | s >= 200 && s < 300 -> do
_bm .= edit_bm'
_edit .= false
Right res -> do
_apiError .= Just (res.body)
liftEffect $ log (res.body)

View file

@ -5,9 +5,10 @@ import Data.Const (Const)
import Effect.Aff (Aff) import Effect.Aff (Aff)
import Foreign.Marked (marked) import Foreign.Marked (marked)
import Halogen as H import Halogen as H
import Halogen.HTML as HH
import Prelude (Void) import Prelude (Void)
type Slot = H.Slot (Const Void) Void type Slot = H.Slot (Const Void) Void
component :: forall q o. H.Component q String o Aff component :: forall q o. H.Component HH.HTML q String o Aff
component = RH.mkComponent marked component = RH.mkComponent marked

View file

@ -12,7 +12,7 @@ import Globals (app', mmoment8601)
import Halogen as H import Halogen as H
import Halogen.HTML (a, br_, div, text) import Halogen.HTML (a, br_, div, text)
import Halogen.HTML as HH import Halogen.HTML as HH
import Halogen.HTML.Properties (href, id, title) import Halogen.HTML.Properties (href, id_, title)
import Model (Note, NoteSlug) import Model (Note, NoteSlug)
import Util (class_, fromNullableStr) import Util (class_, fromNullableStr)
@ -27,7 +27,7 @@ type NLState =
} }
nlist :: forall q i o. Array Note -> H.Component q i o Aff nlist :: forall q i o. Array Note -> H.Component HH.HTML q i o Aff
nlist st' = nlist st' =
H.mkComponent H.mkComponent
{ initialState: const (mkState st') { initialState: const (mkState st')
@ -45,11 +45,11 @@ nlist st' =
} }
render :: NLState -> H.ComponentHTML NLAction () Aff render :: NLState -> H.ComponentHTML NLAction () Aff
render { notes } = render st@{ notes } =
HH.div_ (map renderNote notes) HH.div_ (map renderNote notes)
where where
renderNote note = renderNote note =
div [ id (show note.id) div [ id_ (show note.id)
, class_ ("note w-100 mw7 pa1 mb2" , class_ ("note w-100 mw7 pa1 mb2"
<> if note.shared then "" else " private")] $ <> if note.shared then "" else " private")] $
[ div [ class_ "display" ] $ [ div [ class_ "display" ] $

View file

@ -2,35 +2,29 @@ module Component.NNote where
import Prelude hiding (div) import Prelude hiding (div)
import Affjax (printError)
import Affjax.StatusCode (StatusCode(..))
import App (destroyNote, editNote) import App (destroyNote, editNote)
import Component.Markdown as Markdown import Component.Markdown as Markdown
import Data.Array (drop, foldMap) import Data.Array (drop, foldMap)
import Data.Either (Either(..)) import Data.Either (Either(..))
import Data.Lens (Lens', lens, use, (%=), (.=)) import Data.Lens (Lens', lens, use, (%=), (.=))
import Data.Maybe (Maybe(..), fromMaybe, isJust, maybe) import Data.Maybe (Maybe(..), maybe)
import Data.Monoid (guard) import Data.Monoid (guard)
import Data.String (null, split) as S import Data.String (null, split) as S
import Data.String (null, stripPrefix)
import Data.String.Pattern (Pattern(..)) import Data.String.Pattern (Pattern(..))
import Data.Tuple (fst, snd) import Data.Tuple (fst, snd)
import Effect.Aff (Aff) import Effect.Aff (Aff)
import Effect.Class (liftEffect) import Effect.Class (liftEffect)
import Effect.Console (log) import Globals (app', mmoment8601)
import Globals (app', mmoment8601, setFocus, closeWindow)
import Halogen as H import Halogen as H
import Halogen.HTML (br_, button, div, form, input, label, p, span, text, textarea) import Halogen.HTML (br_, button, div, form, input, label, p, span, text, textarea)
import Halogen.HTML as HH import Halogen.HTML as HH
import Halogen.HTML.Events (onChecked, onClick, onSubmit, onValueChange) import Halogen.HTML.Events (onChecked, onClick, onSubmit, onValueChange)
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autofocus, checked, for, id, name, rows, title, type_, value) import Halogen.HTML.Properties (ButtonType(..), InputType(..), checked, for, id_, name, rows, title, type_, value)
import Model (Note) import Model (Note)
import Type.Proxy (Proxy(..)) import Util (_loc, class_, fromNullableStr, ifElseH)
import Util (_curQuerystring, _doc, _loc, _lookupQueryStringValue, class_, fromNullableStr, ifElseH, whenH)
import Web.Event.Event (Event, preventDefault) import Web.Event.Event (Event, preventDefault)
import Web.HTML (window) import Web.HTML.Location (setHref)
import Web.HTML.HTMLDocument (referrer) import Data.Symbol (SProxy(..))
import Web.HTML.Location (origin, setHref)
data NAction data NAction
= NNop = NNop
@ -46,7 +40,6 @@ type NState =
, deleteAsk :: Boolean , deleteAsk :: Boolean
, edit :: Boolean , edit :: Boolean
, destroyed :: Boolean , destroyed :: Boolean
, apiError :: Maybe String
} }
_note :: Lens' NState Note _note :: Lens' NState Note
@ -58,9 +51,6 @@ _edit_note = lens _.edit_note (_ { edit_note = _ })
_edit :: Lens' NState Boolean _edit :: Lens' NState Boolean
_edit = lens _.edit (_ { edit = _ }) _edit = lens _.edit (_ { edit = _ })
_apiError :: Lens' NState (Maybe String)
_apiError = lens _.apiError (_ { apiError = _ })
-- | FormField Edits -- | FormField Edits
data EditField data EditField
= Etitle String = Etitle String
@ -68,13 +58,13 @@ data EditField
| EisMarkdown Boolean | EisMarkdown Boolean
| Eshared Boolean | Eshared Boolean
_markdown = Proxy :: Proxy "markdown" _markdown = SProxy :: SProxy "markdown"
type ChildSlots = type ChildSlots =
( markdown :: Markdown.Slot Unit ( markdown :: Markdown.Slot Unit
) )
nnote :: forall q i o. Note -> H.Component q i o Aff nnote :: forall q i o. Note -> H.Component HH.HTML q i o Aff
nnote st' = nnote st' =
H.mkComponent H.mkComponent
{ initialState: const (mkState st') { initialState: const (mkState st')
@ -90,11 +80,10 @@ nnote st' =
, deleteAsk: false , deleteAsk: false
, edit: note'.id <= 0 , edit: note'.id <= 0
, destroyed: false , destroyed: false
, apiError: Nothing
} }
render :: NState -> H.ComponentHTML NAction ChildSlots Aff render :: NState -> H.ComponentHTML NAction ChildSlots Aff
render st@{ note, edit_note, apiError } = render st@{ note, edit_note } =
ifElseH st.destroyed ifElseH st.destroyed
display_destroyed display_destroyed
(const (ifElseH st.edit (const (ifElseH st.edit
@ -103,7 +92,7 @@ nnote st' =
where where
renderNote _ = renderNote _ =
div [ id (show note.id) , class_ ("note w-100 mw7 pa1 mb2")] $ div [ id_ (show note.id) , class_ ("note w-100 mw7 pa1 mb2")] $
[ div [ class_ "display" ] $ [ div [ class_ "display" ] $
[ div [ class_ ("link f5 lh-title")] [ div [ class_ ("link f5 lh-title")]
[ text $ if S.null note.title then "[no title]" else note.title ] [ text $ if S.null note.title then "[no title]" else note.title ]
@ -119,42 +108,40 @@ nnote st' =
[ text $ if note.shared then "public" else "private" ] [ text $ if note.shared then "public" else "private" ]
] ]
] ]
, whenH app.dat.isowner $ \_ -> ]
div [ class_ "edit_links db mt3" ] <> -- | Render Action Links
[ button [ type_ ButtonButton, onClick \_ -> NEdit true, class_ "edit light-silver hover-blue" ] [ text "edit  " ] [ div [ class_ "edit_links db mt3" ]
, div [ class_ "delete_link di" ] [ button [ type_ ButtonButton, onClick \_ -> Just (NEdit true), class_ "edit light-silver hover-blue" ] [ text "edit  " ]
[ button [ type_ ButtonButton, onClick \_ -> NDeleteAsk true, class_ ("delete light-silver hover-blue" <> guard st.deleteAsk " dn") ] [ text "delete" ] , div [ class_ "delete_link di" ]
, span ([ class_ ("confirm red" <> guard (not st.deleteAsk) " dn") ] ) [ button [ type_ ButtonButton, onClick \_ -> Just (NDeleteAsk true), class_ ("delete light-silver hover-blue" <> guard st.deleteAsk " dn") ] [ text "delete" ]
[ button [ type_ ButtonButton, onClick \_ -> NDeleteAsk false] [ text "cancel / " ] , span ([ class_ ("confirm red" <> guard (not st.deleteAsk) " dn") ] )
, button [ type_ ButtonButton, onClick \_ -> NDestroy, class_ "red" ] [ text "destroy" ] [ button [ type_ ButtonButton, onClick \_ -> Just (NDeleteAsk false)] [ text "cancel / " ]
] , button [ type_ ButtonButton, onClick \_ -> Just NDestroy, class_ "red" ] [ text "destroy" ]
]
] ]
]
]
] ]
renderNote_edit _ = renderNote_edit _ =
form [ onSubmit NEditSubmit ] form [ onSubmit (Just <<< NEditSubmit) ]
[ whenH (isJust apiError) [ p [ class_ "mt2 mb1"] [ text "title:" ]
(alert_notification (fromMaybe "" apiError)) , input [ type_ InputText , class_ "title w-100 mb1 pt1 f7 edit_form_input" , name "title"
, p [ class_ "mt2 mb1"] [ text "title:" ] , value (edit_note.title) , onValueChange (editField Etitle)
, input [ type_ InputText , class_ "title w-100 mb1 pt1 edit_form_input" , name "title"
, value (edit_note.title) , onValueChange (editField Etitle), autofocus (null edit_note.title)
] ]
, br_ , br_
, p [ class_ "mt2 mb1"] [ text "description:" ] , p [ class_ "mt2 mb1"] [ text "description:" ]
, textarea [ id (notetextid edit_note), class_ "description w-100 mb1 pt1 edit_form_input" , name "text", rows 25 , textarea [ class_ "description w-100 mb1 pt1 f7 edit_form_input" , name "text", rows 30
, value (edit_note.text) , onValueChange (editField Etext) , value (edit_note.text) , onValueChange (editField Etext)
] ]
, div [ class_ "edit_form_checkboxes mb3"] , div [ class_ "edit_form_checkboxes mb3"]
[ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id "edit_ismarkdown", name "ismarkdown" [ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id_ "edit_ismarkdown", name "ismarkdown"
, checked (edit_note.isMarkdown) , onChecked (editField EisMarkdown) ] , checked (edit_note.isMarkdown) , onChecked (editField EisMarkdown) ]
, text " " , text " "
, label [ for "edit_ismarkdown" , class_ "mr2" ] [ text "use markdown?" ] , label [ for "edit_ismarkdown" , class_ "mr2" ] [ text "use markdown?" ]
, br_ , br_
] ]
, div [ class_ "edit_form_checkboxes mb3"] , div [ class_ "edit_form_checkboxes mb3"]
[ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id "edit_shared", name "shared" [ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id_ "edit_shared", name "shared"
, checked (edit_note.shared) , onChecked (editField Eshared) ] , checked (edit_note.shared) , onChecked (editField Eshared) ]
, text " " , text " "
, label [ for "edit_shared" , class_ "mr2" ] [ text "public?" ] , label [ for "edit_shared" , class_ "mr2" ] [ text "public?" ]
@ -167,24 +154,20 @@ nnote st' =
, input [ type_ InputReset , input [ type_ InputReset
, class_ "pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , class_ "pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim"
, value "cancel" , value "cancel"
, onClick \_ -> NEdit false , onClick \_ -> Just (NEdit false)
] ]
] ]
display_destroyed _ = p [ class_ "red"] [text "you killed this note"] display_destroyed _ = p [ class_ "red"] [text "you killed this note"]
alert_notification alert_text _ =
div [ class_ "alert alert-err" ] [ text alert_text ]
mmoment n = mmoment8601 n.created mmoment n = mmoment8601 n.created
editField :: forall a. (a -> EditField) -> a -> NAction editField :: forall a. (a -> EditField) -> a -> Maybe NAction
editField f = NEditField <<< f editField f = Just <<< NEditField <<< f
toTextarea input = toTextarea input =
S.split (Pattern "\n") input S.split (Pattern "\n") input
# foldMap (\x -> [br_, text x]) # foldMap (\x -> [br_, text x])
# drop 1 # drop 1
notetextid note = show note.id <> "_text"
handleAction :: NAction -> H.HalogenM NState NAction ChildSlots o Aff Unit handleAction :: NAction -> H.HalogenM NState NAction ChildSlots o Aff Unit
handleAction (NNop) = pure unit handleAction (NNop) = pure unit
@ -212,38 +195,18 @@ nnote st' =
note <- use _note note <- use _note
_edit_note .= note _edit_note .= note
_edit .= e _edit .= e
qs <- liftEffect _curQuerystring
case {e:e, q:_lookupQueryStringValue qs "next"} of
{e:false, q:Just "closeWindow"} -> liftEffect $ closeWindow =<< window
_ -> H.liftEffect $ whenM (pure e) (setFocus (notetextid note))
-- | Submit -- | Submit
handleAction (NEditSubmit e) = do handleAction (NEditSubmit e) = do
H.liftEffect (preventDefault e) H.liftEffect (preventDefault e)
edit_note <- use _edit_note edit_note <- use _edit_note
_apiError .= Nothing res <- H.liftAff (editNote edit_note)
H.liftAff (editNote edit_note) >>= case _ of case res.body of
Left affErr -> do Left err -> pure unit
_apiError .= Just (printError affErr) Right r -> do
liftEffect $ log (printError affErr) if (edit_note.id == 0)
Right { status: StatusCode s } | s >= 200 && s < 300 -> do then do
qs <- liftEffect _curQuerystring liftEffect (setHref (fromNullableStr app.noteR) =<< _loc)
doc <- liftEffect $ _doc else do
ref <- liftEffect $ referrer doc _note .= edit_note
loc <- liftEffect $ _loc _edit .= false
org <- liftEffect $ origin loc
case _lookupQueryStringValue qs "next" of
Just "closeWindow" -> liftEffect $ closeWindow =<< window
Just "back" -> liftEffect $
if isJust (stripPrefix (Pattern org) ref)
then setHref ref loc
else setHref org loc
_ -> if (edit_note.id == 0)
then liftEffect $ setHref (fromNullableStr app.noteR) =<< _loc
else do
_note .= edit_note
_edit .= false
Right res -> do
_apiError .= Just (res.body)
liftEffect $ log (res.body)

View file

@ -1,5 +1,5 @@
// use at your own risk! // use at your own risk!
export const unsafeSetInnerHTML = function(element) { exports.unsafeSetInnerHTML = function(element) {
return function(html) { return function(html) {
return function() { return function() {
element.innerHTML = html; element.innerHTML = html;

View file

@ -19,7 +19,6 @@ data Action i
= SetInnerHTML = SetInnerHTML
| Receive (Input i) | Receive (Input i)
type Input :: forall k. k -> k
type Input i = i type Input i = i
type State i = type State i =
@ -27,10 +26,10 @@ type State i =
, inputval :: Input i , inputval :: Input i
} }
component :: forall q o. H.Component q (Input String) o Aff component :: forall q o. H.Component HH.HTML q (Input String) o Aff
component = mkComponent RawHTML component = mkComponent RawHTML
mkComponent :: forall q i o. (Input i -> RawHTML) -> H.Component q (Input i) o Aff mkComponent :: forall q i o. (Input i -> RawHTML) -> H.Component HH.HTML q (Input i) o Aff
mkComponent toRawHTML = mkComponent toRawHTML =
H.mkComponent H.mkComponent
{ initialState: \inputval -> { elRef: H.RefLabel "inputval", inputval } { initialState: \inputval -> { elRef: H.RefLabel "inputval", inputval }
@ -54,7 +53,7 @@ mkComponent toRawHTML =
mel <- H.getHTMLElementRef elRef mel <- H.getHTMLElementRef elRef
for_ mel \el -> do for_ mel \el -> do
{ inputval } <- H.get { inputval } <- H.get
H.liftAff $ forkAff $ makeAff \_ -> do H.liftAff $ forkAff $ makeAff \cb -> do
liftEffect $ unsafeSetInnerHTML el (toRawHTML inputval) liftEffect $ unsafeSetInnerHTML el (toRawHTML inputval)
mempty mempty
pure unit pure unit

View file

@ -1,173 +0,0 @@
module Component.TagCloud where
import Prelude hiding (div)
import App (getTagCloud, updateTagCloudMode)
import Data.Array (concat, cons, delete, notElem, null, sortBy)
import Data.Foldable (maximum, minimum)
import Data.Int (toNumber)
import Data.Lens (Lens', lens)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Monoid (guard)
import Data.String (joinWith, toLower, null) as S
import Data.String (toLower)
import Data.Tuple (fst, uncurry)
import Effect.Aff (Aff)
import Foreign.Object (Object)
import Foreign.Object (toUnfoldable, empty, values) as F
import Globals (app')
import Halogen (AttrName(..))
import Halogen as H
import Halogen.HTML (HTML, a, attr, button, div, text)
import Halogen.HTML.Events (onClick)
import Halogen.HTML.Properties (ButtonType(..), href, title, type_)
import Data.Number (log)
import Model (TagCloud, TagCloudModeF(..), isExpanded, isRelated, setExpanded, tagCloudModeFromF)
import Util (class_, encodeTag, fromNullableStr, ifElseA, whenH)
data TAction
= TInitialize
| TExpanded Boolean
| TChangeMode TagCloudModeF
type TState =
{ mode :: TagCloudModeF
, tagcloud :: TagCloud
}
_mode :: Lens' TState TagCloudModeF
_mode = lens _.mode (_ { mode = _ })
tagcloudcomponent :: forall q i o. TagCloudModeF -> H.Component q i o Aff
tagcloudcomponent m' =
H.mkComponent
{ initialState: const (mkState m')
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction
, initialize = Just TInitialize
}
}
where
app = app' unit
mkState m =
{ mode: m
, tagcloud: F.empty
}
render :: TState -> H.ComponentHTML TAction () Aff
render { mode:TagCloudModeNone } =
div [class_ "tag_cloud" ] []
render { mode, tagcloud } =
div [class_ "tag_cloud mv3" ]
[
div [class_ "tag_cloud_header mb2"] $
ifElseA (isRelated mode)
(\_ -> do --RELATED
[ button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue mr1 b")
, onClick \_ -> TExpanded (not (isExpanded mode))
] [text "Related Tags"]
]
)
(\_ -> do -- NOT RELATED
[ button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue mr1" <> guard (mode == modetop) " b")
, title "show a cloud of your most-used tags"
, onClick \_ -> TChangeMode modetop
] [text "Top Tags"]
, button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue ml2 " <> guard (mode == modelb1) " b")
, title "show all tags"
, onClick \_ -> TChangeMode modelb1
] [text "all"]
, text "‧"
, button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue" <> guard (mode == modelb2) " b")
, title "show tags with at least 2 bookmarks"
, onClick \_ -> TChangeMode modelb2
] [text "2"]
, text "‧"
, button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue" <> guard (mode == modelb5) " b")
, title "show tags with at least 5 bookmarks"
, onClick \_ -> TChangeMode modelb5
] [text "5"]
, text "‧"
, button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue" <> guard (mode == modelb10) " b")
, title "show tags with at least 10 bookmarks"
, onClick \_ -> TChangeMode modelb10
] [text "10"]
, text "‧"
, button [ type_ ButtonButton, class_ ("pa1 f7 link hover-blue" <> guard (mode == modelb20) " b")
, title "show tags with at least 20 bookmarks"
, onClick \_ -> TChangeMode modelb20
] [text "20"]
])
<> [button [ type_ ButtonButton, class_ "pa1 ml2 f7 link silver hover-blue "
, onClick \_ -> TExpanded (not (isExpanded mode))]
[ text (if isExpanded mode then "hide" else "show") ]]
, whenH (isExpanded mode) \_ -> do
let n = fromMaybe 1 (minimum (F.values tagcloud))
m = fromMaybe 1 (maximum (F.values tagcloud))
div [class_ "tag_cloud_body"] $ case mode of
TagCloudModeNone -> []
(TagCloudModeRelated _ curtags) ->
toArray curtags n m tagcloud
_ ->
toArray [] n m tagcloud
]
where
modetop = TagCloudModeTop (isExpanded mode) 200
modelb1 = TagCloudModeLowerBound (isExpanded mode) 1
modelb2 = TagCloudModeLowerBound (isExpanded mode) 2
modelb5 = TagCloudModeLowerBound (isExpanded mode) 5
modelb10 = TagCloudModeLowerBound (isExpanded mode) 10
modelb20 = TagCloudModeLowerBound (isExpanded mode) 20
toArray :: Array String -> Int -> Int -> Object Int -> Array (HTML _ _)
toArray curtags n m =
concat
<<< map (uncurry (toSizedTag (map toLower curtags) n m))
<<< sortBy (comparing (S.toLower <<< fst))
<<< F.toUnfoldable
linkToFilterTag rest = fromNullableStr app.userR <> (if S.null rest then "" else "/t:" <> rest)
toSizedTag :: Array String -> Int -> Int -> String -> Int -> _
toSizedTag curtags n m k v =
[ a [ href (linkToFilterTag (encodeTag k)), class_ "link tag mr1" , style]
[ text k ]
, whenH (not (null curtags)) \_ -> if (notElem k_lower curtags)
then a [href (linkToFilterTag (S.joinWith "+" (map encodeTag (cons k_lower curtags)))), class_ "link mr2 tag-include"] [text "⊕"]
else a [href (linkToFilterTag (S.joinWith "+" (map encodeTag (delete k_lower curtags)))), class_ "link mr2 tag-exclude"] [text "⊖"]
]
where
k_lower = toLower k
fontsize = rescale identity (toNumber v) (toNumber n) (toNumber m) 100.0 150.0
opacity = rescale (log <<< (1.0 + _)) (toNumber v) (toNumber n) (toNumber m) 0.6 1.0
style = attr (AttrName "style") ("font-size:" <> show fontsize <> "%" <> ";opacity:" <> show opacity)
rescale :: (Number -> Number) -> Number -> Number -> Number -> Number -> Number -> Number
rescale f v n m l h = (if m - n < 0.01 then 1.0 else (f (v - n) / f (m - n))) * (h - l) + l
fetchTagCloud :: TagCloudModeF -> H.HalogenM TState TAction () o Aff Unit
fetchTagCloud mode' = do
case mode' of
TagCloudModeNone -> pure unit
_ -> do
tagcloud <- H.liftAff $ getTagCloud (tagCloudModeFromF mode')
H.modify_ (\s -> s {
mode = mode',
tagcloud = fromMaybe F.empty tagcloud
})
handleAction :: TAction -> H.HalogenM TState TAction () o Aff Unit
handleAction TInitialize = do
mode <- H.gets _.mode
fetchTagCloud mode
handleAction (TExpanded expanded) = do
H.modify_ (\s -> s { mode = setExpanded s.mode expanded })
mode <- H.gets _.mode
void $ H.liftAff $ updateTagCloudMode (tagCloudModeFromF mode)
handleAction (TChangeMode mode') = do
mode <- H.gets _.mode
if mode == mode'
then handleAction (TExpanded (not (isExpanded mode)))
else fetchTagCloud (setExpanded mode' true)

View file

@ -1,59 +1,67 @@
"use strict"; "use strict";
import moment from 'moment' var moment = require("moment");
export const _app = function() { exports._app = function() {
return app; return app;
} }
export const _closest = function(just, nothing, selector, el) { exports._closest = function(just, nothing, selector, el) {
var node = el.closest(selector); var node = el.closest(selector);
if(node) { if(node) {
return just(node); return just(node);
} else { } else {
return nothing; return nothing;
} }
} }
export const _createFormData = function(formElement) { exports._innerHtml = function(el) {
return new FormData(formElement); return el.innerHTML;
} }
export const _createFormString = function(formElement) { exports._setInnerHtml = function(content, el) {
return new URLSearchParams(new FormData(formElement)).toString() el.innerHTML = content;
} return el;
}
export const _createFormArray = function(formElement) {
return Array.from(new FormData(formElement)); exports._createFormData = function(formElement) {
} return new FormData(formElement);
}
export const _moment8601 = function(tuple, s) {
var m = moment(s, moment.ISO_8601); exports._createFormString = function(formElement) {
var s1 = m.fromNow(); return new URLSearchParams(new FormData(formElement)).toString()
var s2 = m.format('MMMM D YYYY, h:mm a') + " (" + m.format() + ") "; }
return tuple(s1)(s2);
} exports._createFormArray = function(formElement) {
return Array.from(new FormData(formElement));
export const _mmoment8601 = function(just, nothing, tuple, s) { }
try {
var m = moment(s, moment.ISO_8601); exports._getDataAttribute = function(name, el) {
var s1 = m.fromNow(); return el.dataset[name];
var s2 = m.format('MMMM D YYYY, h:mm a') + " (" + m.format() + ") "; }
return just(tuple(s1)(s2));
} catch (error) { exports._setDataAttribute = function(name, value, el) {
return nothing return el.dataset[name] = value;
} }
}
exports._moment8601 = function(tuple, s) {
export const _closeWindow = function (window) { var m = moment(s, moment.ISO_8601);
window.close(); var s1 = m.fromNow();
}; var s2 = m.format('MMMM D YYYY, h:mm a') + " (" + m.format() + ") ";
return tuple(s1)(s2);
export const _setFocus = function(elemId) { }
document.getElementById(elemId).focus();
}; exports._mmoment8601 = function(just, nothing, tuple, s) {
try {
var m = moment(s, moment.ISO_8601);
export const _toLocaleDateString = function(dateString) { var s1 = m.fromNow();
return new Date(dateString).toLocaleDateString(undefined, {dateStyle: 'medium'}) 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();
};

View file

@ -1,87 +1,97 @@
module Globals where module Globals where
import Data.Maybe (Maybe(..)) import Data.Function.Uncurried
import Data.Newtype (class Newtype)
import Data.Nullable (Nullable) import Data.Maybe (Maybe(..))
import Data.Tuple (Tuple(..)) import Data.Nullable (Nullable, toMaybe)
import Effect (Effect) import Data.Tuple (Tuple(..))
import Data.Function.Uncurried (Fn0, Fn1, Fn4, runFn0, runFn1, runFn4) import Effect (Effect)
import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn4, runEffectFn1, runEffectFn2, runEffectFn4) import Model (Bookmark)
import Model (Bookmark) import Prelude (Unit, pure, ($))
import Prelude (Unit) import Web.DOM (Element, Node)
import Web.DOM (Node) import Web.HTML (HTMLElement, HTMLFormElement, Window)
import Web.HTML (HTMLFormElement, Window) import Web.XHR.FormData (FormData)
import Web.XHR.FormData (FormData) import Data.Newtype (class Newtype)
type App = type App =
{ csrfHeaderName :: String { csrfHeaderName :: String
, csrfCookieName :: String , csrfCookieName :: String
, csrfParamName :: String , csrfParamName :: String
, csrfToken :: String , csrfToken :: String
, homeR :: String , homeR :: String
, authRlogoutR :: String , authRlogoutR :: String
, userR :: Nullable String , userR :: Nullable String
, noteR :: Nullable String , noteR :: Nullable String
, dat :: AppData , dat :: AppData
} }
type AppData = type AppData =
{ bmarks :: Array Bookmark { bmarks :: Array Bookmark
, bmark :: Bookmark , bmark :: Bookmark
, isowner :: Boolean , isowner :: Boolean
} }
foreign import _app :: Fn0 App foreign import _app :: Fn0 App
app' :: Unit -> App app' :: Unit -> App
app' _ = runFn0 _app app' _ = runFn0 _app
foreign import _closest :: forall a. EffectFn4 (a -> Maybe a) (Maybe a) String Node (Maybe Node) foreign import _closest :: forall a. Fn4 (a -> Maybe a) (Maybe a) String Node (Maybe Node)
closest :: String -> Node -> Effect (Maybe Node) closest :: String -> Node -> Effect (Maybe Node)
closest selector node = runEffectFn4 _closest Just Nothing selector node closest selector node = pure $ runFn4 _closest Just Nothing selector node
foreign import _moment8601 :: EffectFn2 (String -> String -> Tuple String String) String (Tuple String String) foreign import _moment8601 :: Fn2 (String -> String -> Tuple String String) String (Tuple String String)
moment8601 :: String -> Effect (Tuple String String) moment8601 :: String -> Effect (Tuple String String)
moment8601 s = runEffectFn2 _moment8601 Tuple s 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)) 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 :: String -> Maybe (Tuple String String)
mmoment8601 s = runFn4 _mmoment8601 Just Nothing Tuple s mmoment8601 s = runFn4 _mmoment8601 Just Nothing Tuple s
foreign import _createFormData :: Fn1 HTMLFormElement FormData foreign import _innerHtml :: Fn1 HTMLElement String
createFormData :: HTMLFormElement -> FormData innerHtml :: HTMLElement -> Effect String
createFormData f = runFn1 _createFormData f innerHtml n = pure $ runFn1 _innerHtml n
foreign import _createFormString :: Fn1 HTMLFormElement String foreign import _setInnerHtml :: Fn2 String HTMLElement HTMLElement
createFormString :: HTMLFormElement -> String setInnerHtml :: String -> HTMLElement -> Effect HTMLElement
createFormString f = runFn1 _createFormString f setInnerHtml c n = pure $ runFn2 _setInnerHtml c n
foreign import _createFormData :: Fn1 HTMLFormElement FormData
foreign import _createFormArray :: Fn1 HTMLFormElement (Array (Array String))
createFormData :: HTMLFormElement -> FormData
createFormArray :: HTMLFormElement -> (Array (Array String)) createFormData f = runFn1 _createFormData f
createFormArray f = runFn1 _createFormArray f
foreign import _createFormString :: Fn1 HTMLFormElement String
foreign import _closeWindow :: EffectFn1 Window Unit
createFormString :: HTMLFormElement -> String
closeWindow :: Window -> Effect Unit createFormString f = runFn1 _createFormString f
closeWindow win = runEffectFn1 _closeWindow win
newtype RawHTML = RawHTML String foreign import _createFormArray :: Fn1 HTMLFormElement (Array (Array String))
derive instance newtypeRawHTML :: Newtype RawHTML _ createFormArray :: HTMLFormElement -> (Array (Array String))
createFormArray f = runFn1 _createFormArray f
foreign import _setFocus :: EffectFn1 String Unit
foreign import _getDataAttribute :: Fn2 String Element (Nullable String)
setFocus :: String -> Effect Unit
setFocus s = runEffectFn1 _setFocus s getDataAttribute :: String -> Element -> Effect (Maybe String)
getDataAttribute k n = pure $ toMaybe $ runFn2 _getDataAttribute k n
foreign import _toLocaleDateString :: Fn1 String String
foreign import _setDataAttribute :: Fn3 String String Element Unit
toLocaleDateString :: String -> String
toLocaleDateString s = runFn1 _toLocaleDateString s 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 _

5
purs/src/Main.js Normal file
View file

@ -0,0 +1,5 @@
"use strict";
exports._mainImpl = function() {
return window.PS = PS;
}

View file

@ -1,70 +1,68 @@
module Main where module Main where
import Prelude import Prelude
import App (logout) import App (logout)
import Component.AccountSettings (usetting) import Component.Add (addbmark)
import Component.Add (addbmark) import Component.BList (blist)
import Component.BList (blist) import Component.NList (nlist)
import Component.NList (nlist) import Component.NNote (nnote)
import Component.NNote (nnote) import Component.AccountSettings (usetting)
import Component.TagCloud (tagcloudcomponent) import Data.Foldable (traverse_)
import Data.Foldable (traverse_) import Effect (Effect)
import Effect (Effect) import Effect.Aff (Aff, launchAff)
import Effect.Aff (Aff, launchAff) import Effect.Class (liftEffect)
import Effect.Class (liftEffect) import Halogen.Aff as HA
import Halogen.Aff as HA import Halogen.VDom.Driver (runUI)
import Halogen.VDom.Driver (runUI) import Model (Bookmark, Note, AccountSettings)
import Model (AccountSettings, Bookmark, Note, TagCloudMode, tagCloudModeToF) import Web.DOM.Element (setAttribute)
import Web.DOM.Element (setAttribute) import Web.DOM.ParentNode (QuerySelector(..))
import Web.DOM.ParentNode (QuerySelector(..)) import Web.Event.Event (Event, preventDefault)
import Web.Event.Event (Event, preventDefault) import Web.HTML.HTMLElement (toElement)
import Web.HTML.HTMLElement (toElement)
foreign import _mainImpl :: Effect Unit
logoutE :: Event -> Effect Unit
logoutE e = void <<< launchAff <<< logout =<< preventDefault e main :: Effect Unit
main = _mainImpl
renderBookmarks :: String -> Array Bookmark -> Effect Unit
renderBookmarks renderElSelector bmarks = do logoutE :: Event -> Effect Unit
HA.runHalogenAff do logoutE e = void <<< launchAff <<< logout =<< preventDefault e
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
void $ runUI (blist bmarks) unit el renderBookmarks :: String -> Array Bookmark -> Effect Unit
viewRendered renderBookmarks renderElSelector bmarks = do
HA.runHalogenAff do
renderTagCloud :: String -> TagCloudMode -> Effect Unit HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
renderTagCloud renderElSelector tagCloudMode = do void $ runUI (blist bmarks) unit el
HA.runHalogenAff do viewRendered
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
void $ runUI (tagcloudcomponent (tagCloudModeToF tagCloudMode)) unit el renderAddForm :: String -> Bookmark -> Effect Unit
renderAddForm renderElSelector bmark = do
renderAddForm :: String -> Bookmark -> Effect Unit HA.runHalogenAff do
renderAddForm renderElSelector bmark = do HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
HA.runHalogenAff do void $ runUI (addbmark bmark) unit el
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do viewRendered
void $ runUI (addbmark bmark) unit el
viewRendered renderNotes :: String -> Array Note -> Effect Unit
renderNotes renderElSelector notes = do
renderNotes :: String -> Array Note -> Effect Unit HA.runHalogenAff do
renderNotes renderElSelector notes = do HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
HA.runHalogenAff do void $ runUI (nlist notes) unit el
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do viewRendered
void $ runUI (nlist notes) unit el
viewRendered renderNote :: String -> Note -> Effect Unit
renderNote renderElSelector note = do
renderNote :: String -> Note -> Effect Unit HA.runHalogenAff do
renderNote renderElSelector note = do HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
HA.runHalogenAff do void $ runUI (nnote note) unit el
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do viewRendered
void $ runUI (nnote note) unit el
viewRendered renderAccountSettings :: String -> AccountSettings -> Effect Unit
renderAccountSettings renderElSelector accountSettings = do
renderAccountSettings :: String -> AccountSettings -> Effect Unit HA.runHalogenAff do
renderAccountSettings renderElSelector accountSettings = do HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
HA.runHalogenAff do void $ runUI (usetting accountSettings) unit el
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do viewRendered
void $ runUI (usetting accountSettings) unit el
viewRendered viewRendered :: Aff Unit
viewRendered = HA.selectElement (QuerySelector "#content") >>= traverse_ \el ->
viewRendered :: Aff Unit liftEffect $ setAttribute "view-rendered" "" (toElement el)
viewRendered = HA.selectElement (QuerySelector "#content") >>= traverse_ \el ->
liftEffect $ setAttribute "view-rendered" "" (toElement el)

View file

@ -1,12 +1,11 @@
import { marked } from 'marked'; var marked = require("marked");
import DOMPurify from "dompurify"
marked.setOptions({ marked.setOptions({
pedantic: false, pedantic: false,
gfm: true gfm: true
}); });
export const markedImpl = function(str) { exports.markedImpl = function(str) {
if (!str) return ""; if (!str) return "";
return DOMPurify.sanitize(marked.parse(str)); return marked(str);
}; };

View file

@ -1,129 +1,54 @@
module Model where module Model where
import Control.Monad.Except (runExcept) import Data.Nullable (Nullable)
import Data.Array (intercalate) import Simple.JSON as J
import Data.Either (hush)
import Data.Maybe (fromMaybe) type BookmarkId = Int
import Data.Nullable (Nullable) type TagId = Int
import Data.String (Pattern(..), split)
import Foreign (Foreign, readInt, readString, unsafeToForeign) type Bookmark =
import Foreign.Object (Object) { url :: String
import Prelude (class Eq, pure, ($), (<$>)) , title :: String
import Simple.JSON as J , description :: String
, tags :: String
type BookmarkId = Int , private :: Boolean
type TagId = Int , toread :: Boolean
, bid :: BookmarkId
type Bookmark = , slug :: String
{ url :: String , selected :: Boolean
, title :: String , time :: String
, description :: String , archiveUrl :: Nullable String
, tags :: String }
, private :: Boolean
, toread :: Boolean newtype Bookmark' = Bookmark' Bookmark
, bid :: BookmarkId derive newtype instance bookmark_rfI :: J.ReadForeign Bookmark'
, slug :: String derive newtype instance bookmark_wfI :: J.WriteForeign Bookmark'
, selected :: Boolean
, time :: String type NoteId = Int
, archiveUrl :: Nullable String type NoteSlug = String
}
type Note =
newtype Bookmark' = Bookmark' Bookmark { id :: NoteId
derive newtype instance J.ReadForeign Bookmark' , slug :: NoteSlug
derive newtype instance J.WriteForeign Bookmark' , title :: String
, text :: String
type NoteId = Int , length :: Int
type NoteSlug = String , isMarkdown :: Boolean
, shared :: Boolean
type Note = , created :: String
{ id :: NoteId , updated :: String
, slug :: NoteSlug }
, title :: String
, text :: String newtype Note' = Note' Note
, length :: Int derive newtype instance note_rfI :: J.ReadForeign Note'
, isMarkdown :: Boolean derive newtype instance note_wfI :: J.WriteForeign Note'
, shared :: Boolean
, created :: String type AccountSettings =
, updated :: String { archiveDefault :: Boolean
} , privateDefault :: Boolean
, privacyLock :: Boolean
newtype Note' = Note' Note }
derive newtype instance J.ReadForeign Note'
derive newtype instance J.WriteForeign Note' newtype AccountSettings' = AccountSettings' AccountSettings
derive newtype instance usersettings_rfI :: J.ReadForeign AccountSettings'
type AccountSettings = derive newtype instance usersettings_wfI :: J.WriteForeign AccountSettings'
{ archiveDefault :: Boolean
, privateDefault :: Boolean
, privacyLock :: Boolean
}
newtype AccountSettings' = AccountSettings' AccountSettings
derive newtype instance J.ReadForeign AccountSettings'
derive newtype instance J.WriteForeign AccountSettings'
type TagCloudMode =
{ mode :: String
, value :: Foreign
, expanded :: Boolean
}
newtype TagCloudMode' = TagCloudMode' TagCloudMode
derive newtype instance J.ReadForeign TagCloudMode'
derive newtype instance J.WriteForeign TagCloudMode'
type TagCloud = Object Int
data TagCloudModeF
= TagCloudModeTop Boolean Int
| TagCloudModeLowerBound Boolean Int
| TagCloudModeRelated Boolean (Array String)
| TagCloudModeNone
derive instance Eq TagCloudModeF
tagCloudModeToF :: TagCloudMode -> TagCloudModeF
tagCloudModeToF tagCloudMode =
fromMaybe TagCloudModeNone $ hush $ runExcept $
case tagCloudMode.mode of
"top" -> TagCloudModeTop tagCloudMode.expanded <$> readInt tagCloudMode.value
"lowerBound" -> TagCloudModeLowerBound tagCloudMode.expanded <$> readInt tagCloudMode.value
"related" -> (\s -> TagCloudModeRelated tagCloudMode.expanded (split (Pattern " ") s)) <$> readString tagCloudMode.value
_ -> pure TagCloudModeNone
tagCloudModeFromF :: TagCloudModeF -> TagCloudMode
tagCloudModeFromF (TagCloudModeTop e i) =
{ mode: "top" , value: unsafeToForeign i, expanded: e }
tagCloudModeFromF (TagCloudModeLowerBound e i) =
{ mode: "lowerBound" , value: unsafeToForeign i, expanded: e }
tagCloudModeFromF (TagCloudModeRelated e tags) =
{ mode: "related" , value: unsafeToForeign (intercalate " " tags), expanded: e }
tagCloudModeFromF TagCloudModeNone =
{ mode: "none" , value: unsafeToForeign "", expanded: false }
isExpanded :: TagCloudModeF -> Boolean
isExpanded (TagCloudModeTop e _) = e
isExpanded (TagCloudModeLowerBound e _) = e
isExpanded (TagCloudModeRelated e _) = e
isExpanded TagCloudModeNone = false
isRelated :: TagCloudModeF -> Boolean
isRelated (TagCloudModeRelated _ _) = true
isRelated _ = false
setExpanded :: TagCloudModeF -> Boolean -> TagCloudModeF
setExpanded (TagCloudModeTop _ i) e' = TagCloudModeTop e' i
setExpanded (TagCloudModeLowerBound _ i) e' = TagCloudModeLowerBound e' i
setExpanded (TagCloudModeRelated _ i) e' = TagCloudModeRelated e' i
setExpanded TagCloudModeNone _ = TagCloudModeNone
showMode :: TagCloudModeF -> String
showMode (TagCloudModeTop _ _) = "top"
showMode (TagCloudModeLowerBound _ _) = "lowerBound"
showMode (TagCloudModeRelated _ _) = "related"
showMode TagCloudModeNone = ""
-- isSameMode :: TagCloudModeF -> TagCloudModeF -> Boolean
-- isSameMode (TagCloudModeTop _ _) (TagCloudModeTop _ _) = true
-- isSameMode (TagCloudModeLowerBound _ _) (TagCloudModeLowerBound _ _) = true
-- isSameMode (TagCloudModeRelated _ _) (TagCloudModeRelated _ _) = true
-- isSameMode TagCloudModeNone TagCloudModeNone = true
-- isSameMode _ _ = false

View file

@ -1,155 +1,142 @@
module Util where module Util where
import Prelude import Prelude
import Control.Monad.Maybe.Trans (MaybeT(..)) import Control.Monad.Maybe.Trans (MaybeT(..))
import Data.Array (filter, find, mapMaybe) import Data.Array (filter, find, mapMaybe)
import Data.Foldable (for_) import Data.Foldable (for_)
import Data.Maybe (Maybe(..), fromJust, fromMaybe, maybe) import Data.Maybe (Maybe(..), fromJust, fromMaybe, maybe)
import Data.Nullable (Nullable, toMaybe) import Data.Nullable (Nullable, toMaybe)
import Data.String (Pattern(..), Replacement(..), drop, replaceAll, split, take) import Data.String (Pattern(..), Replacement(..), drop, replaceAll, split, take)
import Data.String as S import Data.Tuple (Tuple(..), fst, snd)
import Data.Tuple (Tuple(..), fst, snd) import Effect (Effect)
import Effect (Effect) import Global.Unsafe (unsafeDecodeURIComponent)
import Halogen (ClassName(..)) import Halogen (ClassName(..))
import Halogen.HTML as HH import Halogen.HTML as HH
import Halogen.HTML.Properties as HP import Halogen.HTML.Properties as HP
import JSURI (decodeURIComponent, encodeURIComponent) import Partial.Unsafe (unsafePartial)
import Partial.Unsafe (unsafePartial) import Web.DOM (Element, Node)
import Web.DOM (Element, Node) import Web.DOM.Document (toNonElementParentNode)
import Web.DOM.Document (toNonElementParentNode) import Web.DOM.Element (fromNode, toParentNode)
import Web.DOM.Element (fromNode, toParentNode) import Web.DOM.NodeList (toArray)
import Web.DOM.NodeList (toArray) import Web.DOM.NonElementParentNode (getElementById)
import Web.DOM.NonElementParentNode (getElementById) import Web.DOM.ParentNode (QuerySelector(..), querySelector, querySelectorAll)
import Web.DOM.ParentNode (QuerySelector(..), querySelector, querySelectorAll) import Web.HTML (HTMLDocument, Location, window)
import Web.HTML (HTMLDocument, Location, window) import Web.HTML.HTMLDocument (body) as HD
import Web.HTML.HTMLDocument (body) as HD import Web.HTML.HTMLDocument (toDocument)
import Web.HTML.HTMLDocument (toDocument) import Web.HTML.HTMLElement (HTMLElement)
import Web.HTML.HTMLElement (HTMLElement) import Web.HTML.HTMLElement (fromElement) as HE
import Web.HTML.HTMLElement (fromElement) as HE import Web.HTML.Location (search)
import Web.HTML.Location (search) import Web.HTML.Window (document, location)
import Web.HTML.Window (document, location)
-- Halogen
unsafeDecode :: String -> String
unsafeDecode str = unsafePartial $ fromJust $ decodeURIComponent str class_ :: forall r i. String -> HP.IProp ( "class" :: String | r) i
class_ = HP.class_ <<< HH.ClassName
-- Halogen
attr :: forall r i. String -> String -> HP.IProp r i
class_ :: forall r i. String -> HP.IProp ( "class" :: String | r) i attr a = HP.attr (HH.AttrName a)
class_ = HP.class_ <<< HH.ClassName
-- Util
attr :: forall r i. String -> String -> HP.IProp r i
attr a = HP.attr (HH.AttrName a) _queryBoth :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Element -> Effect a) -> Effect Unit
_queryBoth (Tuple qa ea) (Tuple qb eb) f = do
-- Util ma <- _querySelector qa ea
mb <- _querySelector qb eb
_queryBoth :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Element -> Effect a) -> Effect Unit for_ ma \a ->
_queryBoth (Tuple qa ea) (Tuple qb eb) f = do for_ mb \b ->
ma <- _querySelector qa ea f a b
mb <- _querySelector qb eb
for_ ma \a -> _queryBoth' :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Array Node -> Effect a) -> Effect Unit
for_ mb \b -> _queryBoth' (Tuple qa ea) (Tuple qb eb) f = do
f a b ma <- _querySelector qa ea
bs <- _querySelectorAll qb eb
_queryBoth' :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Array Node -> Effect a) -> Effect Unit for_ ma \a ->
_queryBoth' (Tuple qa ea) (Tuple qb eb) f = do f a bs
ma <- _querySelector qa ea
bs <- _querySelectorAll qb eb _queryBoth'' :: forall a. Tuple String Element -> Tuple String Element -> (Array Node -> Array Node -> Effect a) -> Effect a
for_ ma \a -> _queryBoth'' (Tuple qa ea) (Tuple qb eb) f = do
f a bs as <- _querySelectorAll qa ea
bs <- _querySelectorAll qb eb
_queryBoth'' :: forall a. Tuple String Element -> Tuple String Element -> (Array Node -> Array Node -> Effect a) -> Effect a f as bs
_queryBoth'' (Tuple qa ea) (Tuple qb eb) f = do
as <- _querySelectorAll qa ea _querySelector :: String -> Element -> Effect (Maybe Element)
bs <- _querySelectorAll qb eb _querySelector s n = querySelector (QuerySelector s) (toParentNode n)
f as bs
_querySelectorAll :: String -> Element -> Effect (Array Node)
_querySelector :: String -> Element -> Effect (Maybe Element) _querySelectorAll s n = toArray =<< querySelectorAll (QuerySelector s) (toParentNode n)
_querySelector s n = querySelector (QuerySelector s) (toParentNode n)
_fromNode :: Node -> Element
_querySelectorAll :: String -> Element -> Effect (Array Node) _fromNode e = unsafePartial $ fromJust (fromNode e)
_querySelectorAll s n = toArray =<< querySelectorAll (QuerySelector s) (toParentNode n)
_fromElement :: Element -> HTMLElement
_fromNode :: Node -> Element _fromElement e = unsafePartial $ fromJust (HE.fromElement e)
_fromNode e = unsafePartial $ fromJust (fromNode e)
_getElementById :: String -> HTMLDocument -> Effect (Maybe Element)
_fromElement :: Element -> HTMLElement _getElementById s = getElementById s <<< toNonElementParentNode <<< toDocument
_fromElement e = unsafePartial $ fromJust (HE.fromElement e)
_doc :: Effect HTMLDocument
_getElementById :: String -> HTMLDocument -> Effect (Maybe Element) _doc = document =<< window
_getElementById s = getElementById s <<< toNonElementParentNode <<< toDocument
_loc :: Effect Location
_doc :: Effect HTMLDocument _loc = location =<< window
_doc = document =<< window
type QueryStringArray = Array (Tuple String (Maybe String))
_loc :: Effect Location
_loc = location =<< window _curQuerystring :: Effect QueryStringArray
_curQuerystring = do
type QueryStringArray = Array (Tuple String (Maybe String)) loc <- _loc
srh <- search loc
_curQuerystring :: Effect QueryStringArray pure $ _parseQueryString srh
_curQuerystring = do
loc <- _loc _parseQueryString :: String -> QueryStringArray
srh <- search loc _parseQueryString srh = do
pure $ _parseQueryString srh let qs = let srh' = take 1 srh in if (srh' == "#" || srh' == "?") then drop 1 srh else srh
mapMaybe go $ (filter (_ /= "") <<< split (Pattern "&")) qs
_parseQueryString :: String -> QueryStringArray where
_parseQueryString srh = do decode = unsafeDecodeURIComponent <<< replaceAll (Pattern "+") (Replacement " ")
let qs = let srh' = take 1 srh in if (srh' == "#" || srh' == "?") then drop 1 srh else srh go kv =
mapMaybe go $ (filter (_ /= "") <<< split (Pattern "&")) qs case split (Pattern "=") kv of
where [k] -> Just (Tuple (decode k) Nothing)
decode = unsafeDecode <<< replaceAll (Pattern "+") (Replacement " ") [k, v] -> Just (Tuple (decode k) (Just (decode v)))
go kv = _ -> Nothing
case split (Pattern "=") kv of
[k] -> Just (Tuple (decode k) Nothing) _lookupQueryStringValue :: QueryStringArray -> String -> Maybe String
[k, v] -> Just (Tuple (decode k) (Just (decode v))) _lookupQueryStringValue qs k = do
_ -> Nothing join $ map snd $ find ((_ == k) <<< fst) qs
_lookupQueryStringValue :: QueryStringArray -> String -> Maybe String _body :: Effect HTMLElement
_lookupQueryStringValue qs k = do _body = unsafePartial $ pure <<< fromJust =<< HD.body =<< _doc
join $ map snd $ find ((_ == k) <<< fst) qs
_mt :: forall a. Effect (Maybe a) -> MaybeT Effect a
_body :: Effect HTMLElement _mt = MaybeT
_body = unsafePartial $ pure <<< fromJust =<< HD.body =<< _doc
_mt_pure :: forall a. Maybe a -> MaybeT Effect a
_mt :: forall a. Effect (Maybe a) -> MaybeT Effect a _mt_pure = MaybeT <<< pure
_mt = MaybeT
dummyAttr :: forall r i. HP.IProp r i
_mt_pure :: forall a. Maybe a -> MaybeT Effect a dummyAttr = HP.attr (HH.AttrName "data-dummy") ""
_mt_pure = MaybeT <<< pure
whenP :: forall r i. Boolean -> HP.IProp r i -> HP.IProp r i
encodeTag :: String -> String whenP b p = if b then p else dummyAttr
encodeTag = fromMaybe "" <<< encodeURIComponent <<< replaceAll (Pattern "+") (Replacement "%2B")
maybeP :: forall a r i. Maybe a -> (a -> HP.IProp r i) -> HP.IProp r i
dummyAttr :: forall r i. HP.IProp r i maybeP m p = maybe dummyAttr p m
dummyAttr = HP.attr (HH.AttrName "data-dummy") ""
whenC :: Boolean -> ClassName -> ClassName
whenP :: forall r i. Boolean -> HP.IProp r i -> HP.IProp r i whenC b c = if b then c else ClassName ""
whenP b p = if b then p else dummyAttr
whenH :: forall p i. Boolean -> (Unit -> HH.HTML p i) -> HH.HTML p i
maybeP :: forall a r i. Maybe a -> (a -> HP.IProp r i) -> HP.IProp r i whenH b k = if b then k unit else HH.text ""
maybeP m p = maybe dummyAttr p m
whenA :: forall t. Boolean -> (Unit -> Array t) -> Array t
whenC :: Boolean -> ClassName -> ClassName whenA b k = if b then k unit else []
whenC b c = if b then c else ClassName ""
ifElseH :: forall p i. Boolean -> (Unit -> HH.HTML p i) -> (Unit -> HH.HTML p i) -> HH.HTML p i
whenH :: forall p i. Boolean -> (Unit -> HH.HTML p i) -> HH.HTML p i ifElseH b f k = if b then f unit else k unit
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
whenA :: forall t. Boolean -> (Unit -> Array t) -> Array t maybeH m k = maybe (HH.text "") k m
whenA b k = if b then k unit else []
fromNullableStr :: Nullable String -> String
ifElseH :: forall p i. Boolean -> (Unit -> HH.HTML p i) -> (Unit -> HH.HTML p i) -> HH.HTML p i fromNullableStr = fromMaybe "" <<< toMaybe
ifElseH b f k = if b then f unit else k unit
ifElseA :: forall t. Boolean -> (Unit -> Array t) -> (Unit -> Array t) -> Array t
ifElseA b f k = if b then f unit else k unit
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
monthNames :: Array String
monthNames = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"]

View file

@ -1,5 +1,4 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}
module Application module Application
( getApplicationDev ( getApplicationDev
@ -16,7 +15,7 @@ module Application
) where ) where
import Control.Monad.Logger (liftLoc, runLoggingT) import Control.Monad.Logger (liftLoc, runLoggingT)
import Database.Persist.Sqlite (ConnectionPool, mkSqliteConnectionInfo, createSqlitePoolFromInfo, fkEnabled, runSqlPool, sqlDatabase, sqlPoolSize) import Database.Persist.Sqlite (createSqlitePool, runSqlPool, sqlDatabase, sqlPoolSize)
import Import import Import
import Language.Haskell.TH.Syntax (qLocation) import Language.Haskell.TH.Syntax (qLocation)
import Lens.Micro import Lens.Micro
@ -29,6 +28,12 @@ import Network.Wai.Middleware.Gzip
import Network.Wai.Middleware.MethodOverride import Network.Wai.Middleware.MethodOverride
import Network.Wai.Middleware.RequestLogger (Destination(Logger), IPAddrSource(..), OutputFormat(..), destination, mkRequestLogger, outputFormat) import Network.Wai.Middleware.RequestLogger (Destination(Logger), IPAddrSource(..), OutputFormat(..), destination, mkRequestLogger, outputFormat)
import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet, toLogStr) import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet, toLogStr)
import Yesod.Auth (getAuth)
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. -- Import all relevant handler modules here.
-- Don't forget to add new modules to your cabal file! -- Don't forget to add new modules to your cabal file!
@ -47,37 +52,38 @@ makeFoundation :: AppSettings -> IO App
makeFoundation appSettings = do makeFoundation appSettings = do
appHttpManager <- getGlobalManager appHttpManager <- getGlobalManager
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
store <- EKG.newStore
EKG.registerGcMetrics store
appMetrics <- MM.initializeWith store
appStatic <- appStatic <-
(if appMutableStatic appSettings (if appMutableStatic appSettings
then staticDevel then staticDevel
else static) else static)
(appStaticDir appSettings) (appStaticDir appSettings)
let mkFoundation appConnPool = App {..} let mkFoundation appConnPool = App { ..}
tempFoundation = mkFoundation (error "connPool forced in tempFoundation") tempFoundation = mkFoundation (error "connPool forced in tempFoundation")
logFunc = messageLoggerSource tempFoundation appLogger logFunc = messageLoggerSource tempFoundation appLogger
pool <- mkPool logFunc True pool <-
poolMigrations <- mkPool logFunc False flip runLoggingT logFunc $
runLoggingT (runSqlPool runMigrations poolMigrations) logFunc createSqlitePool
(sqlDatabase (appDatabaseConf appSettings))
(sqlPoolSize (appDatabaseConf appSettings))
runLoggingT
(runSqlPool runMigrations pool)
logFunc
return (mkFoundation pool) return (mkFoundation pool)
where
mkPool :: _ -> Bool -> IO ConnectionPool
mkPool logFunc isFkEnabled =
flip runLoggingT logFunc $ do
let dbPath = sqlDatabase (appDatabaseConf appSettings)
poolSize = sqlPoolSize (appDatabaseConf appSettings)
connInfo = mkSqliteConnectionInfo dbPath &
set fkEnabled isFkEnabled
createSqlitePoolFromInfo connInfo poolSize
makeApplication :: App -> IO Application makeApplication :: App -> IO Application
makeApplication foundation = do makeApplication foundation = do
logWare <- makeLogWare foundation logWare <- makeLogWare foundation
appPlain <- toWaiAppPlain foundation appPlain <- toWaiAppPlain foundation
return (logWare (makeMiddleware appPlain)) let store = appMetrics foundation ^. MM.metricsStore
waiMetrics <- WM.registerWaiMetrics store
return (logWare (makeMiddleware waiMetrics appPlain))
makeMiddleware :: Middleware makeMiddleware :: WM.WaiMetrics -> Middleware
makeMiddleware = makeMiddleware waiMetrics =
WM.metrics waiMetrics .
acceptOverride . acceptOverride .
autohead . autohead .
gzip def {gzipFiles = GzipPreCompressed GzipIgnore} . gzip def {gzipFiles = GzipPreCompressed GzipIgnore} .
@ -121,6 +127,7 @@ getApplicationDev = do
foundation <- makeFoundation settings foundation <- makeFoundation settings
wsettings <- getDevSettings (warpSettings foundation) wsettings <- getDevSettings (warpSettings foundation)
app <- makeApplication foundation app <- makeApplication foundation
forkEKG foundation
return (wsettings, app) return (wsettings, app)
getAppSettings :: IO AppSettings getAppSettings :: IO AppSettings
@ -130,12 +137,23 @@ getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv
develMain :: IO () develMain :: IO ()
develMain = develMainHelper getApplicationDev 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. -- | The @main@ function for an executable running this site.
appMain :: IO () appMain :: IO ()
appMain = do appMain = do
settings <- loadYamlSettingsArgs [configSettingsYmlValue] useEnv settings <- loadYamlSettingsArgs [configSettingsYmlValue] useEnv
foundation <- makeFoundation settings foundation <- makeFoundation settings
app <- makeApplication foundation app <- makeApplication foundation
forkEKG foundation
runSettings (warpSettings foundation) app runSettings (warpSettings foundation) app
getApplicationRepl :: IO (Int, App, Application) getApplicationRepl :: IO (Int, App, Application)

View file

@ -1,265 +1,251 @@
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE NamedFieldPuns #-} {-# OPTIONS_GHC -fno-warn-unused-matches #-}
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
module Foundation where
module Foundation where
import Import.NoFoundation
import Import.NoFoundation import Database.Persist.Sql (ConnectionPool, runSqlPool)
import Database.Persist.Sql (ConnectionPool, runSqlPool) import Text.Hamlet (hamletFile)
import Text.Hamlet (hamletFile) import Text.Jasmine (minifym)
import PathPiece() import PathPiece()
import Yesod.Core.Types -- import Yesod.Auth.Dummy
import Yesod.Auth.Message
import qualified Data.CaseInsensitive as CI import Yesod.Default.Util (addStaticContentExternal)
import qualified Data.Text.Encoding as TE import Yesod.Core.Types
import qualified Yesod.Core.Unsafe as Unsafe import Yesod.Auth.Message
import qualified Network.Wai as Wai import qualified Network.Wai as NW
import qualified Control.Monad.Metrics as MM
data App = App import qualified Data.CaseInsensitive as CI
{ appSettings :: AppSettings import qualified Data.Text.Encoding as TE
, appStatic :: Static -- ^ Settings for static file serving. import qualified Yesod.Core.Unsafe as Unsafe
, appConnPool :: ConnectionPool -- ^ Database connection pool.
, appHttpManager :: Manager data App = App
, appLogger :: Logger { appSettings :: AppSettings
} deriving (Typeable) , appStatic :: Static -- ^ Settings for static file serving.
, appConnPool :: ConnectionPool -- ^ Database connection pool.
mkYesodData "App" $(parseRoutesFile "config/routes") , appHttpManager :: Manager
, appLogger :: Logger
deriving instance Typeable Route , appMetrics :: !MM.Metrics
deriving instance Generic (Route App) } deriving (Typeable)
-- YesodPersist mkYesodData "App" $(parseRoutesFile "config/routes")
instance YesodPersist App where deriving instance Typeable Route
type YesodPersistBackend App = SqlBackend deriving instance Generic (Route App)
runDB action = do
master <- getYesod -- YesodPersist
runSqlPool action (appConnPool master)
instance YesodPersist App where
instance YesodPersistRunner App where type YesodPersistBackend App = SqlBackend
getDBRunner = defaultGetDBRunner appConnPool runDB action = do
master <- getYesod
session_timeout_minutes :: Int runSqlPool action (appConnPool master)
session_timeout_minutes = 10080 -- (7 days)
instance YesodPersistRunner App where
-- Yesod getDBRunner = defaultGetDBRunner appConnPool
instance Yesod App where -- Yesod
approot = ApprootRequest \app req ->
case appRoot (appSettings app) of instance Yesod App where
Nothing -> getApprootText guessApproot app req approot = ApprootRequest $ \app req ->
Just root -> root case appRoot (appSettings app) of
Nothing -> getApprootText guessApproot app req
makeSessionBackend :: App -> IO (Maybe SessionBackend) Just root -> root
makeSessionBackend App {appSettings} = do
backend <- makeSessionBackend _ = Just <$> defaultClientSessionBackend
defaultClientSessionBackend 10080 -- min (7 days)
session_timeout_minutes "config/client_session_key.aes"
"config/client_session_key.aes"
maybeSSLOnly $ pure (Just backend) yesodMiddleware = metricsMiddleware . defaultYesodMiddleware . defaultCsrfMiddleware
where
maybeSSLOnly = defaultLayout widget = do
if appSSLOnly appSettings req <- getRequest
then sslOnlySessions master <- getYesod
else id urlrender <- getUrlRender
mmsg <- getMessage
yesodMiddleware :: HandlerFor App res -> HandlerFor App res musername <- maybeAuthUsername
yesodMiddleware = customMiddleware . defaultYesodMiddleware . customCsrfMiddleware muser <- (fmap.fmap) snd maybeAuthPair
where mcurrentRoute <- getCurrentRoute
customCsrfMiddleware handler = do void $ mapM (incrementRouteEKG req) mcurrentRoute
maybeRoute <- getCurrentRoute let msourceCodeUri = appSourceCodeUri (appSettings master)
dontCheckCsrf <- case maybeRoute of pc <- widgetToPageContent $ do
-- `maybeAuthId` checks for the validity of the Authorization setTitle "Espial"
-- header anyway, but it is still a good idea to limit this addAppScripts
-- flexibility to designated routes. addStylesheet (StaticR css_tachyons_min_css)
-- For the time being, `AddR` is the only route that accepts an addStylesheet (StaticR css_main_css)
-- authentication token. $(widgetFile "default-layout")
Just AddR -> isJust <$> lookupHeader "Authorization" withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
_ -> pure False
(if dontCheckCsrf then id else defaultCsrfMiddleware) handler addStaticContent ext mime content = do
master <- getYesod
customMiddleware handler = do let staticDir = appStaticDir (appSettings master)
addHeader "X-Frame-Options" "DENY" addStaticContentExternal
yesod <- getYesod minifym
(if appSSLOnly (appSettings yesod) genFileName
then sslOnlyMiddleware session_timeout_minutes staticDir
else id) handler (StaticR . flip StaticRoute [])
ext
defaultLayout widget = do mime
req <- getRequest content
master <- getYesod where
urlrender <- getUrlRender genFileName lbs = "autogen-" ++ base64md5 lbs
mmsg <- getMessage
musername <- maybeAuthUsername shouldLogIO app _source level =
muser <- (fmap.fmap) snd maybeAuthPair pure $ appShouldLogAll (appSettings app) || level == LevelWarn || level == LevelError
let msourceCodeUri = appSourceCodeUri (appSettings master) makeLogger = return . appLogger
pc <- widgetToPageContent do
setTitle "Espial" authRoute _ = Just (AuthR LoginR)
addAppScripts
addStylesheet (StaticR css_tachyons_min_css) isAuthorized (AuthR _) _ = pure Authorized
addStylesheet (StaticR css_main_css) isAuthorized _ _ = pure Authorized
$(widgetFile "default-layout")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") defaultMessageWidget title body = do
setTitle title
shouldLogIO app _source level = toWidget [hamlet|
pure $ appShouldLogAll (appSettings app) || level == LevelWarn || level == LevelError <main .pv2.ph3.mh1>
makeLogger = return . appLogger <div .w-100.mw8.center>
<div .pa3.bg-near-white>
authRoute _ = Just (AuthR LoginR) <h1>#{title}
^{body}
isAuthorized (AuthR _) _ = pure Authorized |]
isAuthorized _ _ = pure Authorized
defaultMessageWidget title body = do isAuthenticated :: Handler AuthResult
setTitle title isAuthenticated = maybeAuthId >>= \case
toWidget [hamlet| Just authId -> pure Authorized
<main .pv2.ph3.mh1> _ -> pure $ AuthenticationRequired
<div .w-100.mw8.center>
<div .pa3.bg-near-white> addAppScripts :: (MonadWidget m, HandlerSite m ~ App) => m ()
<h1>#{title} addAppScripts = do
^{body} addScript (StaticR js_app_min_js)
|]
-- popupLayout
isAuthenticated :: Handler AuthResult
isAuthenticated = maybeAuthId >>= \case popupLayout :: Widget -> Handler Html
Just authId -> pure Authorized popupLayout widget = do
_ -> pure $ AuthenticationRequired req <- getRequest
master <- getYesod
addAppScripts :: (MonadWidget m, HandlerSite m ~ App) => m () mmsg <- getMessage
addAppScripts = do musername <- maybeAuthUsername
addScript (StaticR js_app_min_js) let msourceCodeUri = appSourceCodeUri (appSettings master)
pc <- widgetToPageContent $ do
addAppScripts
-- popupLayout addStylesheet (StaticR css_tachyons_min_css)
addStylesheet (StaticR css_popup_css)
popupLayout :: Widget -> Handler Html $(widgetFile "popup-layout")
popupLayout widget = do withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
req <- getRequest
master <- getYesod
mmsg <- getMessage metricsMiddleware :: Handler a -> Handler a
musername <- maybeAuthUsername metricsMiddleware handler = do
let msourceCodeUri = appSourceCodeUri (appSettings master) req <- getRequest
pc <- widgetToPageContent do mcurrentRoute <- getCurrentRoute
addAppScripts void $ mapM (incrementRouteEKG req) mcurrentRoute
addStylesheet (StaticR css_tachyons_min_css) handler
addStylesheet (StaticR css_popup_css)
$(widgetFile "popup-layout")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") incrementRouteEKG :: YesodRequest -> Route App -> Handler ()
incrementRouteEKG req = MM.increment . (\r -> "route." <> r <> "." <> method) . pack . constrName
where method = decodeUtf8 $ NW.requestMethod $ reqWaiRequest req
-- YesodAuth
-- YesodAuth
instance YesodAuth App where
type AuthId App = UserId instance YesodAuth App where
authPlugins _ = [dbAuthPlugin] type AuthId App = UserId
authenticate = authenticateCreds -- authHttpManager = getHttpManager
loginDest = const HomeR authPlugins _ = [dbAuthPlugin]
logoutDest = const HomeR authenticate = authenticateCreds
onLogin = maybeAuth >>= \case loginDest = const HomeR
Nothing -> cpprint ("onLogin: could not find user" :: Text) logoutDest = const HomeR
Just (Entity _ uname) -> setSession userNameKey (userName uname) onLogin = maybeAuth >>= \case
onLogout = Nothing -> cpprint ("onLogin: could not find user" :: Text)
deleteSession userNameKey Just (Entity _ uname) -> setSession userNameKey (userName uname)
redirectToReferer = const True onLogout =
maybeAuthId = do deleteSession userNameKey
req <- waiRequest redirectToReferer = const True
let mAuthHeader = lookup "Authorization" (Wai.requestHeaders req)
extractKey = stripPrefix "ApiKey " . TE.decodeUtf8 instance YesodAuthPersist App
case mAuthHeader of
Just authHeader -> instance MM.MonadMetrics Handler where
case extractKey authHeader of getMetrics = pure . appMetrics =<< getYesod
Just apiKey -> do
user <- liftHandler $ runDB $ getApiKeyUser (ApiKey apiKey) -- session keys
let userId = entityKey <$> user
pure userId maybeAuthUsername :: Handler (Maybe Text)
-- Since we disable CSRF middleware in the presence of Authorization maybeAuthUsername = do
-- header, we need to explicitly check for the validity of the header lookupSession userNameKey
-- content. Otherwise, a dummy Authorization header with garbage input
-- could be provided to circumvent CSRF token requirement, making the app ultDestKey :: Text
-- vulnerable to CSRF attacks. ultDestKey = "_ULT"
Nothing -> pure Nothing
_ -> defaultMaybeAuthId userNameKey :: Text
userNameKey = "_UNAME"
instance YesodAuthPersist App
-- dbAuthPlugin
-- session keys
dbAuthPluginName :: Text
maybeAuthUsername :: Handler (Maybe Text) dbAuthPluginName = "db"
maybeAuthUsername = do
lookupSession userNameKey dbAuthPlugin :: AuthPlugin App
dbAuthPlugin = AuthPlugin dbAuthPluginName dbDispatch dbLoginHandler
ultDestKey :: Text where
ultDestKey = "_ULT" dbDispatch "POST" ["login"] = dbPostLoginR >>= sendResponse
dbDispatch _ _ = notFound
userNameKey :: Text dbLoginHandler toParent = do
userNameKey = "_UNAME" req <- getRequest
lookupSession ultDestKey >>= \case
-- dbAuthPlugin Just dest | "logout" `isInfixOf` dest -> deleteSession ultDestKey
_ -> pure ()
dbAuthPluginName :: Text setTitle "Espial | Log In"
dbAuthPluginName = "db" $(widgetFile "login")
dbAuthPlugin :: AuthPlugin App dbLoginR :: AuthRoute
dbAuthPlugin = AuthPlugin dbAuthPluginName dbDispatch dbLoginHandler dbLoginR = PluginR dbAuthPluginName ["login"]
where
dbDispatch :: Text -> [Text] -> AuthHandler App TypedContent dbPostLoginR :: AuthHandler master TypedContent
dbDispatch "POST" ["login"] = dbPostLoginR >>= sendResponse dbPostLoginR = do
dbDispatch _ _ = notFound mresult <- runInputPostResult (dbLoginCreds
dbLoginHandler toParent = do <$> ireq textField "username"
req <- getRequest <*> ireq textField "password")
lookupSession ultDestKey >>= \case case mresult of
Just dest | "logout" `isInfixOf` dest -> deleteSession ultDestKey FormSuccess creds -> setCredsRedirect creds
_ -> pure () _ -> loginErrorMessageI LoginR InvalidUsernamePass
setTitle "Espial | Log In"
$(widgetFile "login") dbLoginCreds :: Text -> Text -> Creds master
dbLoginCreds username password =
dbLoginR :: AuthRoute Creds
dbLoginR = PluginR dbAuthPluginName ["login"] { credsPlugin = dbAuthPluginName
, credsIdent = username
dbPostLoginR :: AuthHandler master TypedContent , credsExtra = [("password", password)]
dbPostLoginR = do }
mresult <- runInputPostResult (dbLoginCreds
<$> ireq textField "username" authenticateCreds ::
<*> ireq textField "password") (MonadHandler m, HandlerSite m ~ App)
case mresult of => Creds App
FormSuccess creds -> setCredsRedirect creds -> m (AuthenticationResult App)
_ -> loginErrorMessageI LoginR InvalidUsernamePass authenticateCreds Creds {..} = do
muser <-
dbLoginCreds :: Text -> Text -> Creds master case credsPlugin of
dbLoginCreds username password = p | p == dbAuthPluginName -> liftHandler $ runDB $
Creds join <$> mapM (authenticatePassword credsIdent) (lookup "password" credsExtra)
{ credsPlugin = dbAuthPluginName _ -> pure Nothing
, credsIdent = username case muser of
, credsExtra = [("password", password)] Nothing -> pure (UserError InvalidUsernamePass)
} Just (Entity uid _) -> pure (Authenticated uid)
authenticateCreds :: -- Util
(MonadHandler m, HandlerSite m ~ App)
=> Creds App instance RenderMessage App FormMessage where
-> m (AuthenticationResult App) renderMessage :: App -> [Lang] -> FormMessage -> Text
authenticateCreds Creds {..} = do renderMessage _ _ = defaultFormMessage
muser <-
case credsPlugin of instance HasHttpManager App where
p | p == dbAuthPluginName -> liftHandler $ runDB $ getHttpManager :: App -> Manager
join <$> mapM (\pwd -> authenticatePassword credsIdent pwd) (lookup "password" credsExtra) getHttpManager = appHttpManager
_ -> pure Nothing
case muser of unsafeHandler :: App -> Handler a -> IO a
Nothing -> pure (UserError InvalidUsernamePass) unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
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

View file

@ -2,12 +2,11 @@ module Generic where
import GHC.Generics import GHC.Generics
import ClassyPrelude.Yesod import ClassyPrelude.Yesod
import Data.Kind (Type)
constrName :: (HasConstructor (Rep a), Generic a)=> a -> String constrName :: (HasConstructor (Rep a), Generic a)=> a -> String
constrName = genericConstrName . from constrName = genericConstrName . from
class HasConstructor (f :: Type -> Type) where class HasConstructor (f :: * -> *) where
genericConstrName :: f x -> String genericConstrName :: f x -> String
instance HasConstructor f => HasConstructor (D1 c f) where instance HasConstructor f => HasConstructor (D1 c f) where

View file

@ -8,14 +8,14 @@ getAccountSettingsR = do
(_, user) <- requireAuthPair (_, user) <- requireAuthPair
let accountSettingsEl = "accountSettings" :: Text let accountSettingsEl = "accountSettings" :: Text
let accountSettings = toAccountSettingsForm user let accountSettings = toAccountSettingsForm user
defaultLayout do defaultLayout $ do
$(widgetFile "user-settings") $(widgetFile "user-settings")
toWidgetBody [julius| toWidgetBody [julius|
app.userR = "@{UserR (UserNameP $ userName user)}"; app.userR = "@{UserR (UserNameP $ userName user)}";
app.dat.accountSettings = #{ toJSON accountSettings } || []; app.dat.accountSettings = #{ toJSON accountSettings } || [];
|] |]
toWidget [julius| toWidget [julius|
PS.renderAccountSettings('##{rawJS accountSettingsEl}')(app.dat.accountSettings)(); PS['Main'].renderAccountSettings('##{rawJS accountSettingsEl}')(app.dat.accountSettings)();
|] |]
postEditAccountSettingsR :: Handler () postEditAccountSettingsR :: Handler ()
@ -34,23 +34,17 @@ getChangePasswordR = do
postChangePasswordR :: Handler Html postChangePasswordR :: Handler Html
postChangePasswordR = do postChangePasswordR = do
(userId, user) <- requireAuthPair userId <- requireAuthId
runInputPostResult ((,) <$> ireq textField "oldpassword" <*> ireq textField "newpassword") >>= \case mauthuname <- maybeAuthUsername
FormSuccess (old, new) -> do mresult <- runInputPostResult ((,) <$> ireq textField "oldpassword" <*> ireq textField "newpassword")
runDB (authenticatePassword (userName user) old) >>= \case case (mauthuname, mresult) of
Nothing -> setMessage "Incorrect Old Password" (Just uname, FormSuccess (old, new)) -> do
Just _ -> validateNewPassword new >>= \case muser <- runDB (authenticatePassword uname old)
Just newValid -> do case muser of
newHash <- liftIO (hashPassword newValid) Just _ -> do
void $ runDB (update userId [UserPasswordHash CP.=. newHash]) new' <- liftIO (hashPassword new)
setMessage "Password Changed Successfully" void $ runDB (update userId [UserPasswordHash CP.=. new'])
_ -> pure () setMessage "Password Changed Successfully"
_ -> setMessage "Incorrect Old Password"
_ -> setMessage "Missing Required Fields" _ -> setMessage "Missing Required Fields"
redirect ChangePasswordR redirect ChangePasswordR
validateNewPassword :: Text -> Handler (Maybe Text)
validateNewPassword = \case
new | length new < 6 -> do
setMessage "Password must be at least 6 characters long"
pure Nothing
new -> pure $ Just new

View file

@ -1,90 +1,67 @@
module Handler.Add where module Handler.Add where
import Import import Import
import Handler.Archive import Handler.Archive
import Data.List (nub) import Data.List (nub)
import qualified Data.Text as T (replace)
-- View
-- View
getAddViewR :: Handler Html
getAddViewR :: Handler Html getAddViewR = do
getAddViewR = do userId <- requireAuthId
userId <- requireAuthId
murl <- lookupGetParam "url"
murl <- lookupGetParam "url" mformdb <- runDB (pure . fmap _toBookmarkForm =<< fetchBookmarkByUrl userId murl)
mBookmarkDb <- runDB (fetchBookmarkByUrl userId murl) formurl <- bookmarkFormUrl
let mformdb = fmap _toBookmarkForm mBookmarkDb
formurl <- bookmarkFormUrl let renderEl = "addForm" :: Text
let renderEl = "addForm" :: Text popupLayout $ do
toWidget [whamlet|
popupLayout do <div id="#{ renderEl }">
toWidget [whamlet| |]
<div id="#{ renderEl }"> toWidgetBody [julius|
|] app.dat.bmark = #{ toJSON (fromMaybe formurl mformdb) };
toWidgetBody [julius| |]
app.dat.bmark = #{ toJSON (fromMaybe formurl mformdb) }; toWidget [julius|
|] PS['Main'].renderAddForm('##{rawJS renderEl}')(app.dat.bmark)();
toWidget [julius| |]
PS.renderAddForm('##{rawJS renderEl}')(app.dat.bmark)();
|] bookmarkFormUrl :: Handler BookmarkForm
bookmarkFormUrl = do
bookmarkFormUrl :: Handler BookmarkForm Entity _ user <- requireAuth
bookmarkFormUrl = do BookmarkForm
Entity _ user <- requireAuth <$> (lookupGetParam "url" >>= pure . fromMaybe "")
url <- lookupGetParam "url" <&> fromMaybe "" <*> (lookupGetParam "title")
title <- lookupGetParam "title" <*> (lookupGetParam "description" >>= pure . fmap Textarea)
description <- lookupGetParam "description" <&> fmap Textarea <*> (lookupGetParam "tags")
tags <- lookupGetParam "tags" <*> (lookupGetParam "private" >>= pure . fmap parseChk <&> (<|> Just (userPrivateDefault user)))
private <- lookupGetParam "private" <&> fmap parseChk <&> (<|> Just (userPrivateDefault user)) <*> (lookupGetParam "toread" >>= pure . fmap parseChk)
toread <- lookupGetParam "toread" <&> fmap parseChk <*> pure Nothing
pure $ <*> pure Nothing
BookmarkForm <*> pure Nothing
{ _url = url <*> pure Nothing
, _title = title <*> pure Nothing
, _description = description where
, _tags = tags parseChk s = s == "yes" || s == "on"
, _private = private
, _toread = toread -- API
, _bid = Nothing
, _slug = Nothing postAddR :: Handler ()
, _selected = Nothing postAddR = do
, _time = Nothing bookmarkForm <- requireCheckJsonBody
, _archiveUrl = Nothing _handleFormSuccess bookmarkForm >>= \case
} (Created, bid) -> sendStatusJSON created201 bid
where (Updated, _) -> sendResponseStatus noContent204 ()
parseChk s = s == "yes" || s == "on" || s == "true" || s == "1"
_handleFormSuccess :: BookmarkForm -> Handler (UpsertResult, Key Bookmark)
-- API _handleFormSuccess bookmarkForm = do
(userId, user) <- requireAuthPair
postAddR :: Handler Text bm <- liftIO $ _toBookmark userId bookmarkForm
postAddR = do (res, kbid) <- runDB (upsertBookmark mkbid bm tags)
bookmarkForm <- requireCheckJsonBody whenM (shouldArchiveBookmark user kbid) $
_handleFormSuccess bookmarkForm >>= \case void $ async (archiveBookmarkUrl kbid (unpack (bookmarkHref bm)))
Created bid -> sendStatusJSON created201 bid pure (res, kbid)
Updated _ -> sendResponseStatus noContent204 () where
Failed s -> sendResponseStatus status400 s mkbid = BookmarkKey <$> _bid bookmarkForm
tags = maybe [] (nub . words) (_tags bookmarkForm)
_handleFormSuccess :: BookmarkForm -> Handler (UpsertResult (Key Bookmark))
_handleFormSuccess bookmarkForm = do
(userId, user) <- requireAuthPair
appSettings <- appSettings <$> getYesod
case (appAllowNonHttpUrlSchemes appSettings, (parseRequest . unpack . _url) bookmarkForm) of
(False, Nothing) -> pure $ Failed "Invalid URL"
(_, _) -> do
let mkbid = BookmarkKey <$> _bid bookmarkForm
tags = maybe [] (nub . words . T.replace "," " ") (_tags bookmarkForm)
bm <- liftIO $ _toBookmark userId bookmarkForm
res <- runDB (upsertBookmark userId mkbid bm tags)
forM_ (maybeUpsertResult res) $ \kbid ->
whenM (shouldArchiveBookmark user kbid) $
void $ async (archiveBookmarkUrl kbid (unpack (bookmarkHref bm)))
pure res
postLookupTitleR :: Handler ()
postLookupTitleR = do
void requireAuthId
bookmarkForm <- (requireCheckJsonBody :: Handler BookmarkForm)
fetchPageTitle (unpack (_url bookmarkForm)) >>= \case
Left _ -> sendResponseStatus noContent204 ()
Right title -> sendResponseStatus ok200 title

View file

@ -2,9 +2,7 @@ module Handler.Archive where
import Import import Import
import Data.Function ((&)) import Data.Function ((&))
import Data.Char (ord) import qualified Data.Attoparsec.ByteString.Char8 as AP
import qualified Data.Attoparsec.ByteString.Char8 as AP8
import qualified Data.Attoparsec.ByteString as AP
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Char8 as BS8
@ -12,56 +10,45 @@ import qualified Network.HTTP.Client as NH
import qualified Network.HTTP.Client.TLS as NH import qualified Network.HTTP.Client.TLS as NH
import qualified Network.HTTP.Types.Status as NH import qualified Network.HTTP.Types.Status as NH
import qualified Web.FormUrlEncoded as WH import qualified Web.FormUrlEncoded as WH
import HTMLEntities.Decoder (htmlEncodedText) import qualified Control.Monad.Metrics as MM
import Data.Text.Lazy.Builder (toLazyText)
import Network.Wai (requestHeaderHost)
import qualified Network.Connection as NC
shouldArchiveBookmark :: User -> Key Bookmark -> Handler Bool shouldArchiveBookmark :: User -> Key Bookmark -> Handler Bool
shouldArchiveBookmark user kbid = shouldArchiveBookmark user kbid = do
runDB (get kbid) >>= \case runDB (get kbid) >>= \case
Nothing -> pure False Nothing -> pure False
Just bm -> do
Just bm -> do pure $
pure $ (isNothing $ bookmarkArchiveHref bm) &&
isNothing (bookmarkArchiveHref bm) && (bookmarkShared bm)
bookmarkShared bm && not (_isArchiveBlacklisted bm)
&& not (_isArchiveBlacklisted bm) && not (userPrivacyLock user)
&& userArchiveDefault user && userArchiveDefault user
getArchiveManager :: Handler Manager
getArchiveManager = do
appSettings <- appSettings <$> getYesod
let mSocks =
NC.SockSettingsSimple <$>
fmap unpack (appArchiveSocksProxyHost appSettings) <*>
fmap toEnum (appArchiveSocksProxyPort appSettings)
NH.newTlsManagerWith (NH.mkManagerSettings def mSocks)
archiveBookmarkUrl :: Key Bookmark -> String -> Handler () archiveBookmarkUrl :: Key Bookmark -> String -> Handler ()
archiveBookmarkUrl kbid url = archiveBookmarkUrl kbid url =
(_fetchArchiveSubmitInfo >>= \case (_fetchArchiveSubmitInfo >>= \case
Left e -> do Left e -> do
MM.increment "archive.fetchSubmitId_noparse"
$(logError) (pack e) $(logError) (pack e)
Right submitInfo -> do Right submitInfo -> do
userId <- requireAuthId userId <- requireAuthId
req <- _buildArchiveSubmitRequest submitInfo url let req = _buildArchiveSubmitRequest submitInfo url
manager <- getArchiveManager MM.increment "archive.submit"
res <- liftIO $ NH.httpLbs req manager res <- liftIO $ NH.httpLbs req =<< NH.getGlobalManager
let status = NH.responseStatus res let status = NH.responseStatus res
let updateArchiveUrl url' = runDB $ updateBookmarkArchiveUrl userId kbid $ Just url' MM.increment ("archive.submit_status_" <> (pack.show) (NH.statusCode status))
let updateArchiveUrl = runDB . updateBookmarkArchiveUrl userId kbid . Just
headers = NH.responseHeaders res headers = NH.responseHeaders res
case status of case status of
s | s == NH.status200 -> s | s == NH.status200 ->
for_ (lookup "Refresh" headers >>= _parseRefreshHeaderUrl) updateArchiveUrl for_ (lookup "Refresh" headers >>= _parseRefreshHeaderUrl) updateArchiveUrl
s | s == NH.status302 || s == NH.status307 -> s | s == NH.status302 ->
for_ (lookup "Location" headers) (updateArchiveUrl . decodeUtf8) for_ (lookup "Location" headers) (updateArchiveUrl . decodeUtf8)
_ -> $(logError) (pack (show res))) _ -> $(logError) (pack (show res)))
`catch` (\(e::SomeException) -> ($(logError) $ (pack.show) e) >> throwIO e) `catch` (\(e::SomeException) -> ($(logError) $ (pack.show) e) >> throwIO e)
_isArchiveBlacklisted :: Bookmark -> Bool _isArchiveBlacklisted :: Bookmark -> Bool
_isArchiveBlacklisted Bookmark {..} = _isArchiveBlacklisted (Bookmark {..}) =
[ "hulu" [ "hulu"
, "livestream" , "livestream"
, "netflix" , "netflix"
@ -77,73 +64,43 @@ _isArchiveBlacklisted Bookmark {..} =
_parseRefreshHeaderUrl :: ByteString -> Maybe Text _parseRefreshHeaderUrl :: ByteString -> Maybe Text
_parseRefreshHeaderUrl h = do _parseRefreshHeaderUrl h = do
let u = BS8.drop 1 $ BS8.dropWhile (/= '=') h let u = BS8.drop 1 $ BS8.dropWhile (/= '=') h
if not (null u) if (not (null u))
then Just $ decodeUtf8 u then Just $ decodeUtf8 u
else Nothing else Nothing
_fetchArchiveSubmitInfo :: Handler (Either String (String , String)) _buildArchiveSubmitRequest :: (String, String) -> String -> NH.Request
_fetchArchiveSubmitInfo = do _buildArchiveSubmitRequest (action, submitId) href =
req <- buildRequest "https://archive.li/" NH.parseRequest_ ("POST " <> action) & \r ->
manager <- getArchiveManager
res <- liftIO $ NH.httpLbs req manager
let body = LBS.toStrict (responseBody res)
action = _parseSubstring (AP8.string "action=\"") (AP8.notChar '"') body
submitId = _parseSubstring (AP8.string "submitid\" value=\"") (AP8.notChar '"') body
if statusCode (responseStatus res) == 200
then pure $ (,) <$> action <*> submitId
else pure $ Left $ "Invalid statusCode: " <> show (responseStatus res)
_parseSubstring :: AP8.Parser ByteString -> AP8.Parser Char -> BS.ByteString -> Either String String
_parseSubstring start inner = AP8.parseOnly (skipAnyTill start >> AP8.many1 inner)
where
skipAnyTill end = go where go = end $> () <|> AP8.anyChar *> go
fetchPageTitle :: String -> Handler (Either String Text)
fetchPageTitle url =
do
req <- buildRequest url
res <- liftIO $ NH.httpLbs req =<< NH.getGlobalManager
let body = LBS.toStrict (responseBody res)
pure (decodeHtmlBs <$> parseTitle body)
`catch` (\(e :: SomeException) -> do
$(logError) $ (pack . show) e
pure (Left (show e)))
where
parseTitle bs =
flip AP.parseOnly bs do
_ <- skipAnyTill (AP.string "<title")
_ <- skipAnyTill (AP.string ">")
let lt = toEnum (ord '<')
AP.takeTill (== lt)
decodeHtmlBs = toStrict . toLazyText . htmlEncodedText . decodeUtf8
skipAnyTill end = go where go = end $> () <|> AP.anyWord8 *> go
_buildArchiveSubmitRequest :: (String, String) -> String -> Handler NH.Request
_buildArchiveSubmitRequest (action, submitId) href = do
req <- buildRequest ("POST " <> action)
pure $ req
{ NH.requestHeaders = ("Content-Type", "application/x-www-form-urlencoded") : NH.requestHeaders req
, NH.requestBody =
NH.RequestBodyLBS $
WH.urlEncodeAsForm
([("submitid", submitId), ("url", href)] :: [(String, String)])
, NH.redirectCount = 0
}
buildRequest :: String -> Handler Request
buildRequest url = do
ua <- _archiveUserAgent
pure $ NH.parseRequest_ url & \r ->
r { NH.requestHeaders = r { NH.requestHeaders =
[ ("Cache-Control", "max-age=0") [ ("User-Agent", _archiveUserAgent)
, ("User-Agent", ua) , ("Content-Type", "application/x-www-form-urlencoded")
] ]
, NH.requestBody = NH.RequestBodyLBS $ WH.urlEncodeAsForm ((
[ ("submitid" , submitId)
, ("url", href)
]) :: [(String, String)])
, NH.redirectCount = 0
} }
_archiveUserAgent :: Handler ByteString _fetchArchiveSubmitInfo :: Handler (Either String (String , String))
_archiveUserAgent = do _fetchArchiveSubmitInfo = do
mHost <- requestHeaderHost . reqWaiRequest <$> getRequest MM.increment "archive.fetchSubmitId"
pure $ "espial-" <> maybe "" (BS8.takeWhile (/= ':')) mHost 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

View file

@ -5,7 +5,6 @@ import Import
import Data.FileEmbed (embedFile) import Data.FileEmbed (embedFile)
import Text.Read import Text.Read
import Data.Aeson as A
-- These handlers embed files in the executable at compile time to avoid a -- These handlers embed files in the executable at compile time to avoid a
-- runtime dependency, and for efficiency. -- runtime dependency, and for efficiency.
@ -22,49 +21,11 @@ getRobotsR = return $ TypedContent typePlain
lookupPagingParams :: Handler (Maybe Int64, Maybe Int64) lookupPagingParams :: Handler (Maybe Int64, Maybe Int64)
lookupPagingParams = lookupPagingParams = do
(,) cq <- fmap parseMaybe (lookupGetParam "count")
<$> getUrlSessionParam "count" cs <- fmap parseMaybe (lookupSession "count")
<*> getUrlParam "page" for_ cq (setSession "count" . (pack . show))
pq <- fmap parseMaybe (lookupGetParam "page")
getUrlParam :: (Read a) => Text -> Handler (Maybe a) pure (cq <|> cs, pq)
getUrlParam name = do
fmap parseMaybe (lookupGetParam name)
where where
parseMaybe x = readMaybe . unpack =<< x parseMaybe x = readMaybe . unpack =<< x
getUrlSessionParam :: forall a.
(Show a, Read a)
=> Text
-> Handler (Maybe a)
getUrlSessionParam name = do
p <- fmap parseMaybe (lookupGetParam name)
s <- fmap parseMaybe (lookupSession name)
for_ p (setSession name . (pack . show))
pure (p <|> s)
where
parseMaybe :: Maybe Text -> Maybe a
parseMaybe x = readMaybe . unpack =<< x
lookupTagCloudMode :: MonadHandler m => m (Maybe TagCloudMode)
lookupTagCloudMode = do
(A.decode . fromStrict =<<) <$> lookupSessionBS "tagCloudMode"
setTagCloudMode :: MonadHandler m => TagCloudMode -> m ()
setTagCloudMode = setSessionBS "tagCloudMode" . toStrict . A.encode
getTagCloudMode :: MonadHandler m => Bool -> [Tag] -> m TagCloudMode
getTagCloudMode isowner tags = do
ms <- lookupTagCloudMode
let expanded = maybe False isExpanded ms
pure $
if not isowner
then TagCloudModeNone
else if not (null tags)
then TagCloudModeRelated expanded tags
else case ms of
Nothing -> TagCloudModeTop expanded 200
Just (TagCloudModeRelated e _) -> TagCloudModeTop e 200
Just m -> m

View file

@ -5,5 +5,5 @@ module Handler.Docs where
import Import import Import
getDocsSearchR :: Handler Html getDocsSearchR :: Handler Html
getDocsSearchR = popupLayout getDocsSearchR = popupLayout $
$(widgetFile "docs-search") $(widgetFile "docs-search")

View file

@ -11,16 +11,16 @@ import Import
deleteDeleteR :: Int64 -> Handler Html deleteDeleteR :: Int64 -> Handler Html
deleteDeleteR bid = do deleteDeleteR bid = do
userId <- requireAuthId userId <- requireAuthId
runDB do runDB $ do
let k_bid = BookmarkKey bid let k_bid = BookmarkKey bid
_ <- requireResource userId k_bid _ <- requireResource userId k_bid
delete k_bid deleteCascade k_bid
return "" return ""
postReadR :: Int64 -> Handler Html postReadR :: Int64 -> Handler Html
postReadR bid = do postReadR bid = do
userId <- requireAuthId userId <- requireAuthId
runDB do runDB $ do
let k_bid = BookmarkKey bid let k_bid = BookmarkKey bid
_ <- requireResource userId k_bid _ <- requireResource userId k_bid
update k_bid [BookmarkToRead =. False] update k_bid [BookmarkToRead =. False]
@ -37,7 +37,7 @@ postUnstarR bid = _setSelected bid False
_setSelected :: Int64 -> Bool -> Handler Html _setSelected :: Int64 -> Bool -> Handler Html
_setSelected bid selected = do _setSelected bid selected = do
userId <- requireAuthId userId <- requireAuthId
runDB do runDB $ do
let k_bid = BookmarkKey bid let k_bid = BookmarkKey bid
bm <- requireResource userId k_bid bm <- requireResource userId k_bid
update k_bid [BookmarkSelected =. selected] update k_bid [BookmarkSelected =. selected]

View file

@ -1,5 +1,4 @@
{-# OPTIONS_GHC -fno-warn-unused-matches #-} {-# OPTIONS_GHC -fno-warn-unused-matches #-}
{-# LANGUAGE TupleSections #-}
module Handler.Notes where module Handler.Notes where
import Import import Import
@ -7,28 +6,25 @@ import Handler.Common (lookupPagingParams)
import qualified Data.Aeson as A import qualified Data.Aeson as A
import qualified Data.Text as T import qualified Data.Text as T
import Yesod.RssFeed import Yesod.RssFeed
import Text.Blaze.Html (toHtml)
import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5 as H
import qualified Network.Wai.Internal as W
getNotesR :: UserNameP -> Handler Html getNotesR :: UserNameP -> Handler Html
getNotesR unamep@(UserNameP uname) = do getNotesR unamep@(UserNameP uname) = do
mauthuname <- maybeAuthUsername muserid <- maybeAuthId
(limit', page') <- lookupPagingParams (limit', page') <- lookupPagingParams
let queryp = "query" let queryp = "query" :: Text
mquery <- lookupGetParam queryp mquery <- lookupGetParam queryp
let limit = maybe 20 fromIntegral limit' let limit = maybe 20 fromIntegral limit'
page = maybe 1 fromIntegral page' page = maybe 1 fromIntegral page'
mqueryp = fmap (queryp,) mquery mqueryp = fmap (\q -> (queryp, q)) mquery
isowner = Just uname == mauthuname (bcount, notes) <- runDB $ do
(bcount, notes) <- runDB do Entity userId _ <- getBy404 (UniqueUserName uname)
Entity userId user <- getBy404 (UniqueUserName uname) let sharedp = if muserid == Just userId then SharedAll else SharedPublic
let sharedp = if isowner then SharedAll else SharedPublic
when (not isowner && userPrivacyLock user)
(redirect (AuthR LoginR))
getNoteList userId mquery sharedp limit page getNoteList userId mquery sharedp limit page
req <- getRequest req <- getRequest
mroute <- getCurrentRoute mroute <- getCurrentRoute
defaultLayout do defaultLayout $ do
rssLink (NotesFeedR unamep) "feed" rssLink (NotesFeedR unamep) "feed"
let pager = $(widgetFile "pager") let pager = $(widgetFile "pager")
search = $(widgetFile "search") search = $(widgetFile "search")
@ -37,74 +33,60 @@ getNotesR unamep@(UserNameP uname) = do
toWidgetBody [julius| toWidgetBody [julius|
app.userR = "@{UserR unamep}"; app.userR = "@{UserR unamep}";
app.dat.notes = #{ toJSON notes } || []; app.dat.notes = #{ toJSON notes } || [];
app.dat.isowner = #{ isowner };
|] |]
toWidget [julius| toWidget [julius|
PS.renderNotes('##{rawJS renderEl}')(app.dat.notes)(); PS['Main'].renderNotes('##{rawJS renderEl}')(app.dat.notes)();
|] |]
getNoteR :: UserNameP -> NtSlug -> Handler Html getNoteR :: UserNameP -> NtSlug -> Handler Html
getNoteR unamep@(UserNameP uname) slug = do getNoteR unamep@(UserNameP uname) slug = do
mauthuname <- maybeAuthUsername
let renderEl = "note" :: Text let renderEl = "note" :: Text
isowner = Just uname == mauthuname
note <- note <-
runDB $ runDB $
do Entity userId user <- getBy404 (UniqueUserName uname) do Entity userId _ <- getBy404 (UniqueUserName uname)
mnote <- getNote userId slug mnote <- getNote userId slug
note <- maybe notFound pure mnote maybe notFound pure mnote
when (not isowner && (userPrivacyLock user || (not . noteShared . entityVal) note)) defaultLayout $ do
(redirect (AuthR LoginR))
pure note
defaultLayout do
$(widgetFile "note") $(widgetFile "note")
toWidgetBody [julius| toWidgetBody [julius|
app.userR = "@{UserR unamep}"; app.userR = "@{UserR unamep}";
app.dat.note = #{ toJSON note } || []; app.dat.note = #{ toJSON note } || [];
app.dat.isowner = #{ isowner };
|] |]
toWidget [julius| toWidget [julius|
PS.renderNote('##{rawJS renderEl}')(app.dat.note)(); PS['Main'].renderNote('##{rawJS renderEl}')(app.dat.note)();
|] |]
getAddNoteSlimViewR :: Handler Html
getAddNoteSlimViewR = do
Entity userId user <- requireAuth
getAddNoteViewR (UserNameP (userName user))
getAddNoteViewR :: UserNameP -> Handler Html getAddNoteViewR :: UserNameP -> Handler Html
getAddNoteViewR unamep@(UserNameP uname) = do getAddNoteViewR unamep@(UserNameP uname) = do
userId <- requireAuthId userId <- requireAuthId
note <- liftIO . _toNote userId =<< noteFormUrl
let renderEl = "note" :: Text let renderEl = "note" :: Text
enote = Entity (NoteKey 0) note note <- liftIO $ Entity (NoteKey 0) <$> _toNote userId (NoteForm Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing)
defaultLayout do defaultLayout $ do
$(widgetFile "note") $(widgetFile "note")
toWidgetBody [julius| toWidgetBody [julius|
app.userR = "@{UserR unamep}"; app.userR = "@{UserR unamep}";
app.noteR = "@{NoteR unamep (noteSlug (entityVal enote))}"; app.noteR = "@{NoteR unamep (noteSlug (entityVal note))}";
app.dat.note = #{ toJSON enote } || []; app.dat.note = #{ toJSON note } || [];
|] |]
toWidget [julius| toWidget [julius|
PS.renderNote('##{rawJS renderEl}')(app.dat.note)(); PS['Main'].renderNote('##{rawJS renderEl}')(app.dat.note)();
|] |]
deleteDeleteNoteR :: Int64 -> Handler Html deleteDeleteNoteR :: Int64 -> Handler Html
deleteDeleteNoteR nid = do deleteDeleteNoteR nid = do
userId <- requireAuthId userId <- requireAuthId
runDB do runDB $ do
let k_nid = NoteKey nid let k_nid = NoteKey nid
_ <- requireResource userId k_nid _ <- requireResource userId k_nid
delete k_nid deleteCascade k_nid
return "" return ""
postAddNoteR :: Handler Text postAddNoteR :: Handler ()
postAddNoteR = do postAddNoteR = do
noteForm <- requireCheckJsonBody noteForm <- requireCheckJsonBody
_handleFormSuccess noteForm >>= \case _handleFormSuccess noteForm >>= \case
Created nid -> sendStatusJSON created201 nid (Created, nid) -> sendStatusJSON created201 nid
Updated _ -> sendResponseStatus noContent204 () (Updated, _) -> sendResponseStatus noContent204 ()
Failed s -> sendResponseStatus status400 s
requireResource :: UserId -> Key Note -> DBM Handler Note requireResource :: UserId -> Key Note -> DBM Handler Note
requireResource userId k_nid = do requireResource userId k_nid = do
@ -113,11 +95,11 @@ requireResource userId k_nid = do
then return nnote then return nnote
else notFound else notFound
_handleFormSuccess :: NoteForm -> Handler (UpsertResult (Key Note)) _handleFormSuccess :: NoteForm -> Handler (UpsertResult, Key Note)
_handleFormSuccess noteForm = do _handleFormSuccess noteForm = do
userId <- requireAuthId userId <- requireAuthId
note <- liftIO $ _toNote userId noteForm note <- liftIO $ _toNote userId noteForm
runDB (upsertNote userId knid note) runDB (upsertNote knid note)
where where
knid = NoteKey <$> (_id noteForm >>= \i -> if i > 0 then Just i else Nothing) knid = NoteKey <$> (_id noteForm >>= \i -> if i > 0 then Just i else Nothing)
@ -138,90 +120,52 @@ instance ToJSON NoteForm where toJSON = A.genericToJSON gNoteFormOptions
gNoteFormOptions :: A.Options gNoteFormOptions :: A.Options
gNoteFormOptions = A.defaultOptions { A.fieldLabelModifier = drop 1 } gNoteFormOptions = A.defaultOptions { A.fieldLabelModifier = drop 1 }
noteFormUrl :: Handler NoteForm
noteFormUrl = do
title <- lookupGetParam "title"
description <- lookupGetParam "description" <&> fmap Textarea
isMarkdown <- lookupGetParam "isMarkdown" <&> fmap parseChk
pure $ NoteForm
{ _id = Nothing
, _slug = Nothing
, _title = title
, _text = description
, _isMarkdown = isMarkdown
, _shared = Nothing
, _created = Nothing
, _updated = Nothing
}
where
parseChk s = s == "yes" || s == "on" || s == "true" || s == "1"
_toNote :: UserId -> NoteForm -> IO Note _toNote :: UserId -> NoteForm -> IO Note
_toNote userId NoteForm {..} = do _toNote userId NoteForm {..} = do
time <- liftIO getCurrentTime time <- liftIO getCurrentTime
slug <- maybe mkNtSlug pure _slug slug <- maybe mkNtSlug pure _slug
pure $ pure $
Note Note
{ noteUserId = userId userId
, noteSlug = slug slug
, noteLength = length _text (length _text)
, noteTitle = fromMaybe "" _title (fromMaybe "" _title)
, noteText = maybe "" unTextarea _text (maybe "" unTextarea _text)
, noteIsMarkdown = Just True == _isMarkdown (fromMaybe False _isMarkdown)
, noteShared = Just True == _shared (fromMaybe False _shared)
, noteCreated = maybe time unUTCTimeStr _created (fromMaybe time (fmap unUTCTimeStr _created))
, noteUpdated = maybe time unUTCTimeStr _updated (fromMaybe time (fmap unUTCTimeStr _updated))
}
noteToRssEntry :: (Route App -> Text) -> UserNameP -> Entity Note -> FeedEntry Text noteToRssEntry :: UserNameP -> Entity Note -> FeedEntry (Route App)
noteToRssEntry render usernamep (Entity entryId entry) = noteToRssEntry usernamep (Entity entryId entry) =
FeedEntry FeedEntry { feedEntryLink = NoteR usernamep (noteSlug entry)
{ feedEntryLink = render $ NoteR usernamep (noteSlug entry) , feedEntryUpdated = (noteUpdated entry)
, feedEntryUpdated = noteUpdated entry , feedEntryTitle = (noteTitle entry)
, feedEntryTitle = noteTitle entry , feedEntryContent = (toHtml (noteText entry))
, feedEntryContent = toHtml (noteText entry) , feedEntryEnclosure = Nothing
, feedEntryEnclosure = Nothing }
, feedEntryCategories = []
}
getNotesFeedR :: UserNameP -> Handler RepRss getNotesFeedR :: UserNameP -> Handler RepRss
getNotesFeedR unamep@(UserNameP uname) = do getNotesFeedR unamep@(UserNameP uname) = do
mauthuname <- maybeAuthUsername
(limit', page') <- lookupPagingParams (limit', page') <- lookupPagingParams
mquery <- lookupGetParam "query" let queryp = "query" :: Text
mquery <- lookupGetParam queryp
let limit = maybe 20 fromIntegral limit' let limit = maybe 20 fromIntegral limit'
page = maybe 1 fromIntegral page' page = maybe 1 fromIntegral page'
isowner = Just uname == mauthuname (bcount, notes) <- runDB $ do
sharedp = if isowner then SharedAll else SharedPublic Entity userId _ <- getBy404 (UniqueUserName uname)
(_, notes) <- runDB do getNoteList userId mquery SharedPublic limit page
Entity userId user <- getBy404 (UniqueUserName uname)
when (not isowner && userPrivacyLock user)
(redirect (AuthR LoginR))
getNoteList userId mquery sharedp limit page
render <- getUrlRender
let (descr :: Html) = toHtml $ H.text (uname <> " notes") let (descr :: Html) = toHtml $ H.text (uname <> " notes")
entries = map (noteToRssEntry render unamep) notes let entries = map (noteToRssEntry unamep) notes
updated <- case maximumMay (map feedEntryUpdated entries) of updated <- case maximumMay (map feedEntryUpdated entries) of
Nothing -> liftIO getCurrentTime Nothing -> liftIO $ getCurrentTime
Just m -> return m Just m -> return m
(feedLinkSelf, feedLinkHome) <- getFeedLinkSelf rssFeed $ Feed (uname <> " notes")
rssFeedText $ (NotesFeedR unamep)
Feed (NotesR unamep)
{ feedTitle = uname <> " notes" uname
, feedLinkSelf = feedLinkSelf descr
, feedLinkHome = feedLinkHome "en"
, feedAuthor = uname updated
, feedDescription = descr Nothing
, feedLanguage = "en" entries
, feedUpdated = updated
, feedLogo = Nothing
, feedEntries = entries
}
where
getFeedLinkSelf = do
request <- getRequest
render <- getUrlRender
let rawRequest = reqWaiRequest request
feedLinkSelf = render HomeR <> (T.drop 1 (decodeUtf8 (W.rawPathInfo rawRequest <> W.rawQueryString rawRequest)))
feedLinkHome = render (UserR unamep)
pure (feedLinkSelf, feedLinkHome)

View file

@ -1,172 +1,105 @@
{-# OPTIONS_GHC -fno-warn-unused-matches #-} {-# OPTIONS_GHC -fno-warn-unused-matches #-}
{-# LANGUAGE TupleSections #-} module Handler.User where
module Handler.User where
import qualified Data.Text as T
import qualified Data.Text as T import Handler.Common (lookupPagingParams)
import Handler.Common import Import
import Import import Text.Blaze.Html (toHtml)
import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5 as H
import Yesod.RssFeed import Yesod.RssFeed
import qualified Data.Map as Map
import qualified Network.Wai.Internal as W getUserR :: UserNameP -> Handler Html
getUserR uname@(UserNameP name) = do
getUserR :: UserNameP -> Handler Html _getUser uname SharedAll FilterAll (TagsP [])
getUserR uname=
_getUser uname SharedAll FilterAll (TagsP []) getUserSharedR :: UserNameP -> SharedP -> Handler Html
getUserSharedR uname sharedp =
getUserSharedR :: UserNameP -> SharedP -> Handler Html _getUser uname sharedp FilterAll (TagsP [])
getUserSharedR uname sharedp =
_getUser uname sharedp FilterAll (TagsP []) getUserFilterR :: UserNameP -> FilterP -> Handler Html
getUserFilterR uname filterp =
getUserFilterR :: UserNameP -> FilterP -> Handler Html _getUser uname SharedAll filterp (TagsP [])
getUserFilterR uname filterp =
_getUser uname SharedAll filterp (TagsP []) getUserTagsR :: UserNameP -> TagsP -> Handler Html
getUserTagsR uname pathtags =
getUserTagsR :: UserNameP -> TagsP -> Handler Html _getUser uname SharedAll FilterAll pathtags
getUserTagsR uname = _getUser uname SharedAll FilterAll
_getUser :: UserNameP -> SharedP -> FilterP -> TagsP -> Handler Html
_getUser :: UserNameP -> SharedP -> FilterP -> TagsP -> Handler Html _getUser unamep@(UserNameP uname) sharedp' filterp' (TagsP pathtags) = do
_getUser unamep@(UserNameP uname) sharedp' filterp' (TagsP pathtags) = do mauthuname <- maybeAuthUsername
mauthuname <- maybeAuthUsername (limit', page') <- lookupPagingParams
(limit', page') <- lookupPagingParams let limit = maybe 120 fromIntegral limit'
let limit = maybe 120 fromIntegral limit' page = maybe 1 fromIntegral page'
page = maybe 1 fromIntegral page' isowner = maybe False (== uname) mauthuname
isowner = Just uname == mauthuname sharedp = if isowner then sharedp' else SharedPublic
sharedp = if isowner then sharedp' else SharedPublic filterp = case filterp' of
filterp = case filterp' of FilterSingle _ -> filterp'
FilterSingle _ -> filterp' _ -> if isowner then filterp' else FilterAll
_ -> if isowner then filterp' else FilterAll isAll = filterp == FilterAll && sharedp == SharedAll && pathtags == []
isAll = filterp == FilterAll && sharedp == SharedAll && null pathtags queryp = "query" :: Text
queryp = "query" :: Text mquery <- lookupGetParam queryp
mquery <- lookupGetParam queryp let mqueryp = fmap (\q -> (queryp, q)) mquery
let mqueryp = fmap (queryp,) mquery (bcount, bmarks, alltags) <-
(bcount, btmarks) <- runDB $ do runDB $
Entity userId user <- getBy404 (UniqueUserName uname) do Entity userId user <- getBy404 (UniqueUserName uname)
when (not isowner && userPrivacyLock user) when (not isowner && userPrivacyLock user)
(redirect (AuthR LoginR)) (redirect (AuthR LoginR))
bookmarksTagsQuery userId sharedp filterp pathtags mquery limit page (cnt, bm) <- bookmarksQuery userId sharedp filterp pathtags mquery limit page
when (bcount == 0) (case filterp of FilterSingle _ -> notFound; _ -> pure ()) tg <- tagsQuery bm
mroute <- getCurrentRoute pure (cnt, bm, tg)
tagCloudMode <- getTagCloudMode isowner pathtags when (bcount == 0) (case filterp of FilterSingle _ -> notFound; _ -> pure ())
req <- getRequest mroute <- getCurrentRoute
defaultLayout do req <- getRequest
let pager = $(widgetFile "pager") defaultLayout $ do
search = $(widgetFile "search") let pager = $(widgetFile "pager")
renderEl = "bookmarks" :: Text search = $(widgetFile "search")
tagCloudRenderEl = "tagCloud" :: Text renderEl = "bookmarks" :: Text
rssLink (UserFeedR unamep) "feed" rssLink (UserFeedR unamep) "feed"
$(widgetFile "user") $(widgetFile "user")
toWidgetBody [julius| toWidgetBody [julius|
app.dat.bmarks = #{ toJSON $ toBookmarkFormList btmarks } || []; app.dat.bmarks = #{ toJSON $ toBookmarkFormList bmarks alltags } || [];
app.dat.isowner = #{ isowner }; app.dat.isowner = #{ isowner };
app.userR = "@{UserR unamep}"; app.userR = "@{UserR unamep}";
app.tagCloudMode = #{ toJSON $ tagCloudMode } || {}; |]
|] toWidget [julius|
toWidget [julius| PS['Main'].renderBookmarks('##{rawJS renderEl}')(app.dat.bmarks)();
setTimeout(() => { |]
PS.renderBookmarks('##{rawJS renderEl}')(app.dat.bmarks)();
}, 0); bookmarkToRssEntry :: Entity Bookmark -> FeedEntry Text
setTimeout(() => { bookmarkToRssEntry (Entity entryId entry) =
PS.renderTagCloud('##{rawJS tagCloudRenderEl}')(app.tagCloudMode)(); FeedEntry { feedEntryLink = (bookmarkHref entry)
}, 0); , feedEntryUpdated = (bookmarkTime entry)
|] , feedEntryTitle = (bookmarkDescription entry)
, feedEntryContent = (toHtml (bookmarkExtended entry))
-- Form , feedEntryEnclosure = Nothing
}
postUserTagCloudR :: Handler ()
postUserTagCloudR = do getUserFeedR :: UserNameP -> Handler RepRss
userId <- requireAuthId getUserFeedR unamep@(UserNameP uname) = do
mode <- requireCheckJsonBody mauthuname <- maybeAuthUsername
_updateTagCloudMode mode (limit', page') <- lookupPagingParams
tc <- runDB $ case mode of let limit = maybe 120 fromIntegral limit'
TagCloudModeTop _ n -> tagCountTop userId n page = maybe 1 fromIntegral page'
TagCloudModeLowerBound _ n -> tagCountLowerBound userId n queryp = "query" :: Text
TagCloudModeRelated _ tags -> tagCountRelated userId tags mquery <- lookupGetParam queryp
TagCloudModeNone -> notFound (bcount, bmarks, alltags) <-
sendStatusJSON ok200 (Map.fromList tc :: Map.Map Text Int) runDB $
do Entity userId user <- getBy404 (UniqueUserName uname)
postUserTagCloudModeR :: Handler () (cnt, bm) <- bookmarksQuery userId SharedPublic FilterAll [] mquery limit page
postUserTagCloudModeR = do tg <- tagsQuery bm
userId <- requireAuthId pure (cnt, bm, tg)
mode <- requireCheckJsonBody let (descr :: Html) = toHtml $ H.text ("Bookmarks saved by " <> uname)
_updateTagCloudMode mode let entries = map bookmarkToRssEntry bmarks
updated <- case maximumMay (map feedEntryUpdated entries) of
_updateTagCloudMode :: TagCloudMode -> Handler () Nothing -> liftIO $ getCurrentTime
_updateTagCloudMode mode = Just m -> return m
case mode of render <- getUrlRender
TagCloudModeTop _ _ -> setTagCloudMode mode rssFeedText $ Feed ("espial " <> uname)
TagCloudModeLowerBound _ _ -> setTagCloudMode mode (render (UserFeedR unamep))
TagCloudModeRelated _ _ -> setTagCloudMode mode (render (UserR unamep))
TagCloudModeNone -> notFound uname
descr
bookmarkToRssEntry :: (Entity Bookmark, Maybe Text) -> FeedEntry Text "en"
bookmarkToRssEntry (Entity entryId entry, tags) = updated
FeedEntry Nothing
{ feedEntryLink = bookmarkHref entry entries
, feedEntryUpdated = bookmarkTime entry
, feedEntryTitle = bookmarkDescription entry
, feedEntryContent = toHtml (bookmarkExtended entry)
, feedEntryCategories = map (EntryCategory Nothing Nothing) (maybe [] words tags)
, feedEntryEnclosure = Nothing
}
getUserFeedR :: UserNameP -> Handler RepRss
getUserFeedR unamep = do
_getUserFeed unamep SharedAll FilterAll (TagsP [])
getUserFeedSharedR :: UserNameP -> SharedP -> Handler RepRss
getUserFeedSharedR uname sharedp =
_getUserFeed uname sharedp FilterAll (TagsP [])
getUserFeedFilterR :: UserNameP -> FilterP -> Handler RepRss
getUserFeedFilterR uname filterp =
_getUserFeed uname SharedAll filterp (TagsP [])
getUserFeedTagsR :: UserNameP -> TagsP -> Handler RepRss
getUserFeedTagsR uname = _getUserFeed uname SharedAll FilterAll
_getUserFeed :: UserNameP -> SharedP -> FilterP -> TagsP -> Handler RepRss
_getUserFeed 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 = Just 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 && null pathtags
queryp = "query" :: Text
mquery <- lookupGetParam queryp
(_, btmarks) <- runDB $ do
Entity userId user <- getBy404 (UniqueUserName uname)
when (not isowner && userPrivacyLock user)
(redirect (AuthR LoginR))
bookmarksTagsQuery userId sharedp filterp pathtags mquery limit page
let (descr :: Html) = toHtml $ H.text ("Bookmarks saved by " <> uname)
entries = map bookmarkToRssEntry btmarks
updated <- case maximumMay (map feedEntryUpdated entries) of
Nothing -> liftIO getCurrentTime
Just m -> return m
(feedLinkSelf, feedLinkHome) <- getFeedLinkSelf
rssFeedText $
Feed
{ feedTitle = "espial " <> uname
, feedLinkSelf = feedLinkSelf
, feedLinkHome = feedLinkHome
, feedAuthor = uname
, feedDescription = descr
, feedLanguage = "en"
, feedUpdated = updated
, feedLogo = Nothing
, feedEntries = entries
}
where
getFeedLinkSelf = do
request <- getRequest
render <- getUrlRender
let rawRequest = reqWaiRequest request
feedLinkSelf = render HomeR <> (T.drop 1 (decodeUtf8 (W.rawPathInfo rawRequest <> W.rawQueryString rawRequest)))
feedLinkHome = render (UserR unamep)
pure (feedLinkSelf, feedLinkHome)

View file

@ -48,13 +48,13 @@ aFormToMaybeGetSuccess
:: MonadHandler f :: MonadHandler f
=> AForm f a -> f (Maybe a) => AForm f a -> f (Maybe a)
aFormToMaybeGetSuccess = aFormToMaybeGetSuccess =
fmap (maybeSuccess . fst) . runFormGet . const . fmap fst . aFormToForm fmap maybeSuccess . fmap fst . runFormGet . const . fmap fst . aFormToForm
aFormToMaybePostSuccess aFormToMaybePostSuccess
:: MonadHandlerForm f :: MonadHandlerForm f
=> AForm f a -> f (Maybe a) => AForm f a -> f (Maybe a)
aFormToMaybePostSuccess = aFormToMaybePostSuccess =
fmap (maybeSuccess . fst) . runFormPostNoToken . const . fmap fst . aFormToForm fmap maybeSuccess . fmap fst . runFormPostNoToken . const . fmap fst . aFormToForm
maybeSuccess :: FormResult a -> Maybe a maybeSuccess :: FormResult a -> Maybe a
maybeSuccess (FormSuccess a) = Just a maybeSuccess (FormSuccess a) = Just a
@ -83,4 +83,4 @@ attrs n f =
} }
cls :: [Text] -> FieldSettings master -> FieldSettings master cls :: [Text] -> FieldSettings master -> FieldSettings master
cls n = attrs [("class", unwords n)] cls n = attrs [("class", intercalate " " n)]

File diff suppressed because it is too large Load diff

View file

@ -12,8 +12,6 @@ import qualified Data.Aeson as A
import System.Entropy (getEntropy) import System.Entropy (getEntropy)
import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Base64.URL as Base64Url
import qualified Crypto.Hash.SHA256 as SHA256
mkSlug :: Int -> IO T.Text mkSlug :: Int -> IO T.Text
mkSlug size = mkSlug size =
@ -60,18 +58,3 @@ hashPassword rawPassword = do
validatePasswordHash :: BCrypt -> T.Text -> Bool validatePasswordHash :: BCrypt -> T.Text -> Bool
validatePasswordHash hash' pass = do validatePasswordHash hash' pass = do
validatePassword (TE.encodeUtf8 (unBCrypt hash')) (TE.encodeUtf8 pass) validatePassword (TE.encodeUtf8 (unBCrypt hash')) (TE.encodeUtf8 pass)
newtype ApiKey = ApiKey { unApiKey :: T.Text }
newtype HashedApiKey
= HashedApiKey T.Text
deriving stock (Eq, Ord, Show)
deriving newtype (PersistField, PersistFieldSql, A.FromJSON, A.ToJSON)
generateApiKey :: IO ApiKey
generateApiKey = do
bytes <- getEntropy 32
pure $ ApiKey $ Base64Url.encodeBase64 bytes
hashApiKey :: ApiKey -> HashedApiKey
hashApiKey = HashedApiKey . TE.decodeUtf8 . Base64Url.encodeBase64' . SHA256.hash . TE.encodeUtf8 . unApiKey

View file

@ -1,61 +1,55 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module PathPiece where module PathPiece where
import Data.Text (breakOn, splitOn) import Data.Text (splitOn)
import qualified Data.Text as T (replace)
import Import.NoFoundation import Import.NoFoundation
-- PathPiece -- PathPiece
instance PathPiece UserNameP where instance PathPiece UserNameP where
toPathPiece (UserNameP i) = "u:" <> i toPathPiece (UserNameP i) = "u:" <> i
fromPathPiece s = fromPathPiece s =
case breakOn ":" s of case splitOn ":" s of
("u", "") -> Nothing ["u", ""] -> Nothing
("u", uname) -> Just $ UserNameP (drop 1 uname) ["u", uname] -> Just $ UserNameP uname
_ -> Nothing _ -> Nothing
instance PathPiece TagsP where instance PathPiece TagsP where
toPathPiece (TagsP tags) = "t:" <> intercalate "+" (fmap encodeTag tags) toPathPiece (TagsP tags) = "t:" <> (intercalate "+" tags)
fromPathPiece s = fromPathPiece s =
case breakOn ":" s of case splitOn ":" s of
("t", "") -> Nothing ["t", ""] -> Nothing
("t", tags) -> Just $ (TagsP . fmap decodeTag . splitOn "+" . drop 1) tags ["t", tags] -> Just $ TagsP (splitOn "+" tags)
_ -> Nothing _ -> Nothing
encodeTag :: Text -> Text instance PathPiece SharedP where
encodeTag = T.replace "+" "%2B" toPathPiece = \case
SharedAll -> ""
decodeTag :: Text -> Text SharedPublic -> "public"
decodeTag = T.replace "%2B" "+" SharedPrivate -> "private"
fromPathPiece = \case
instance PathPiece SharedP where "public" -> Just SharedPublic
toPathPiece = \case "private" -> Just SharedPrivate
SharedAll -> "" _ -> Nothing
SharedPublic -> "public"
SharedPrivate -> "private" instance PathPiece FilterP where
fromPathPiece = \case toPathPiece = \case
"public" -> Just SharedPublic FilterAll -> ""
"private" -> Just SharedPrivate FilterUnread -> "unread"
_ -> Nothing FilterUntagged -> "untagged"
FilterStarred -> "starred"
instance PathPiece FilterP where FilterSingle slug -> "b:" <> unBmSlug slug
toPathPiece = \case fromPathPiece = \case
FilterAll -> "" "unread" -> Just FilterUnread
FilterUnread -> "unread" "untagged" -> Just FilterUntagged
FilterUntagged -> "untagged" "starred" -> Just FilterStarred
FilterStarred -> "starred" s -> case splitOn ":" s of
FilterSingle slug -> "b:" <> unBmSlug slug ["b", ""] -> Nothing
fromPathPiece = \case ["b", slug] -> Just $ FilterSingle (BmSlug slug)
"unread" -> Just FilterUnread _ -> Nothing
"untagged" -> Just FilterUntagged
"starred" -> Just FilterStarred
s -> case breakOn ":" s of deriving instance PathPiece NtSlug
("b", "") -> Nothing deriving instance PathPiece BmSlug
("b", slug) -> Just $ FilterSingle (BmSlug (drop 1 slug))
_ -> Nothing
deriving instance PathPiece NtSlug
deriving instance PathPiece BmSlug

View file

@ -56,22 +56,16 @@ data AppSettings = AppSettings
, appAuthDummyLogin :: Bool , appAuthDummyLogin :: Bool
-- ^ Indicate if auth dummy login should be enabled. -- ^ Indicate if auth dummy login should be enabled.
, appArchiveSocksProxyHost :: Maybe Text , appEkgHost :: Maybe Text
-- ^ Socks proxy host to use when making archive requests -- ^ Host/interface the ekg server should bind to.
, appEkgPort :: Maybe Int
, appArchiveSocksProxyPort :: Maybe Int -- ^ Port to listen on
-- ^ Socks proxy port to use when making archive requests
, appSourceCodeUri :: Maybe Text , appSourceCodeUri :: Maybe Text
-- ^ Uri to app source code -- ^ Uri to app source code
, appSSLOnly :: Bool
, appAllowNonHttpUrlSchemes :: Bool
} }
instance FromJSON AppSettings where instance FromJSON AppSettings where
parseJSON = withObject "AppSettings" \o -> do parseJSON = withObject "AppSettings" $ \o -> do
let defaultDev = let defaultDev =
#ifdef DEVELOPMENT #ifdef DEVELOPMENT
True True
@ -98,13 +92,9 @@ instance FromJSON AppSettings where
appAuthDummyLogin <- o .:? "auth-dummy-login" .!= dev appAuthDummyLogin <- o .:? "auth-dummy-login" .!= dev
appArchiveSocksProxyHost <- o .:? "archive-socks-proxy-host" appEkgHost <- o .:? "ekg-host"
appArchiveSocksProxyPort <- o .:? "archive-socks-proxy-port" appEkgPort <- o .:? "ekg-port"
appSourceCodeUri <- o .:? "source-code-uri" appSourceCodeUri <- o .:? "source-code-uri"
appSSLOnly <- fromMaybe False <$> o .:? "ssl-only"
appAllowNonHttpUrlSchemes <- fromMaybe False <$> o .:? "allow-non-http-url-schemes"
return AppSettings {..} return AppSettings {..}

View file

@ -1,3 +1,9 @@
resolver: lts-20.1 resolver: lts-14.3
# allow-newer: true
extra-deps:
- ekg-0.4.0.15
- ekg-json-0.1.0.6
- monad-metrics-0.2.1.4
- wai-middleware-metrics-0.2.4
packages: packages:
- . - '.'

View file

@ -3,10 +3,38 @@
# For more information, please see the documentation at: # For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/lock_files # https://docs.haskellstack.org/en/stable/lock_files
packages: [] packages:
- completed:
hackage: ekg-0.4.0.15@sha256:f35b2c6d80415314f84056afaba6e622bf8d90eb01d0504c87f04c64cb105e04,2030
pantry-tree:
size: 1495
sha256: f9f8f92d73fd4cc8efe37b5a3db009a8c195e590ab9f7d680582ca253123ab3a
original:
hackage: ekg-0.4.0.15
- completed:
hackage: ekg-json-0.1.0.6@sha256:4ff2e9cac213a5868ae8b4a7c72a16a9a76fac14d944ae819b3d838a9725569b,1050
pantry-tree:
size: 265
sha256: 77dde8082700d78a353b7e476e4457aaa41acf62b1b60dbdbb450dfd553cf9b5
original:
hackage: ekg-json-0.1.0.6
- completed:
hackage: monad-metrics-0.2.1.4@sha256:ec7be46f0693b1acb0d7cad114b33f418eb82447f3a6bc90b19f695ff3a6d718,1914
pantry-tree:
size: 457
sha256: 07d623e9b2ebf8c4a5f2210ff8117d53c6aab05bfe7ac2ecd4c990cba4046096
original:
hackage: monad-metrics-0.2.1.4
- completed:
hackage: wai-middleware-metrics-0.2.4@sha256:d6b6916acd41aaef4ca59a839d40a3a377f9df784ae49fd4c64926ae916b6ba2,2890
pantry-tree:
size: 330
sha256: 99366b831109417cd8e739fb45e9fd214cb79f28a507f8154e5528120042d0ac
original:
hackage: wai-middleware-metrics-0.2.4
snapshots: snapshots:
- completed: - completed:
sha256: b73b2b116143aea728c70e65c3239188998bac5bc3be56465813dacd74215dc5 size: 523878
size: 648424 url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/14/3.yaml
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/1.yaml sha256: 470c46c27746a48c7c50f829efc0cf00112787a7804ee4ac7a27754658f6d92c
original: lts-20.1 original: lts-14.3

View file

@ -1,6 +1,38 @@
@media (prefers-color-scheme: dark) { :root {
html, img, video, iframe { filter: invert(1); } color-scheme: light dark; /* support color scheme */
body { background-color: white; } --base03: #002b36;
--base02: #073642;
--base01: #586e75;
--base00: #657b83;
--base0: #839496;
--base1: #93a1a1;
--base2: #eee8d5;
--base3: #fdf6e3;
--yellow: #b58900;
--orange: #cb4b16;
--red: #dc322f;
--magenta: #d33682;
--violet: #6c71c4;
--blue: #268bd2;
--cyan: #2aa198;
--green: #859900;
--transparent: rgba(255,255,255,0);
/* --main-background: #00151b; /* 0.5 darker than #002b36; */
--main-background: var(--base03); /* 0.5 darker than #002b36; */
--main-foreground: var(--base1);
--second-foreground: var(--base0);
--reveal-background: var(--base02);
--soft-foreground: var(--base01);
--border-color: var(--base02);
--todo-txt: #000;
--color-h1: var(--cyan);
--color-h2: var(--green);
--color-h3: var(--yellow);
--color-h4: var(--orange);
--color-h5: var(--red);
--color-h6: var(--magenta);
--color-link: var(--magenta);
} }
html { html {
@ -10,63 +42,74 @@ html {
body { body {
height: 102%; height: 102%;
word-wrap: break-word; word-wrap: break-word;
background-color: var(--main-background);
color: var(--main-foreground);
} }
a {
color: var(--blue);
}
a:visited { color: var(--violet);}
a:hover, a:active, .top_menu a:hover {color: var(--magenta);}
.bg-white { background-color: var(--main-background); }
input, textarea {
background-color: var(--reveal-background);
color: var(--main-foreground);
border: solid 1px var(--border-color);
}
.gold { color: var(--yellow); }
.hover-orange:hover { color: var(--orange); }
button, .button {color: white;}
.bg-white input:focus, .bg-white textarea:focus {
border: solid 1px var(--blue);
box-shadow: none;
}
button { button {
background: none; background:none;
border: none; border:none;
padding: 0; padding:0;
cursor: pointer; cursor:pointer;
} }
button:focus { button:focus {
outline: none; outline: none;
} }
[hidden] { [hidden] {
display: none !important; display: none !important
} }
input::placeholder { input::placeholder {
color: lightgray; color: var(--soft-foreground);
} }
input[type="text"],
input[type="url"],
input[type="password"],
textarea {
font-size: 16px;
}
.queryInput { .queryInput {
width: 128px; width: 128px;
padding: 0 22px 0 2px; padding: 0 22px 0 2px;
border-radius: 3px; border-radius: 3px;
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
border-color: gray; border-color: var(--border-color);
height: 22px; height: 22px;
line-height: 22px; line-height: 22px;
transition: width 0.1s ease-in-out; transition: width .1s ease-in-out
}
.queryInput.search-inactive {
} }
.queryInput.search-inactive {}
.queryInput:focus { .queryInput:focus {
width: 175px; width: 175px;
} }
.submitting .queryInput, .submitting .queryInput,
.queryInput.search-active { .queryInput.search-active {
border-color: #990; border-color: var(--border-color);
border-width: 2px; border-width: 2px;
background-color: #ff9; background-color: var(--reveal-background);
width: 175px; width: 175px;
} }
.queryIcon { .queryIcon {
position: absolute; position: absolute;
right: 0; right: 0;
top: 1px; top:1px;
cursor: pointer; cursor:pointer;
width: 20px; width:20px;
height: 20px; height: 20px;
fill: currentColor; fill: currentColor;
} }
@ -74,121 +117,89 @@ label {
cursor: pointer; cursor: pointer;
} }
.close-x-wrap { .close-x-wrap {
float: left; float: left;
width: 17px; width: 17px;
height: 17px; height: 17px;
top: 2px; top: 2px;
position: relative; position: relative;
right: 2px; right: 2px;
} }
.close-x { .close-x {
stroke: gray; stroke: var(--second-foreground);
fill: transparent; fill: transparent;
stroke-linecap: round; stroke-linecap: round;
stroke-width: 3; stroke-width: 3;
} }
.query-info-icon { .query-info-icon {
position: absolute; position: absolute;
top: 0px; top: 0px;
right: -18px; right: -18px;
text-decoration: none; text-decoration: none;
font-size: 12px; font-size: 12px;
padding: 0 8px 8px 0; padding: 0 8px 8px 0;
} }
.star { .star {
margin-left: -20px; margin-left:-20px;
font-size: 1.2em; font-size:1.2em;
position: relative; position:relative;
top: -2px; top:-2px;
} }
.star button { .star button {
transition: color 0.1s; transition: color .1s;
} }
.star.selected button { .star.selected button {
color: #22a; color:var(--magenta);
} }
.edit_links button { .edit_links button {
transition: color 0.1s ease-in; transition: color .1s ease-in;
} }
.tag { .tag {
color: #a51; color:var(--cyan);
line-height: 190%; line-height:190%;
display: inline-block;
}
.tag-include {
color: rgb(221, 221, 221);
line-height: 190%;
display: inline-block;
}
.tag-exclude {
color: rgb(255, 170, 170);
line-height: 190%;
display: inline-block;
} }
.private { .private { background:var(--reveal-background);border:1px solid var(--border-color); }
background: #ddd; .unread { color:var(--yellow); }
border: 1px solid #d1d1d1; .mark_read {color: var(--yellow);}
} .flash { color:var(--green); }
.unread {
color: #b41;
}
.mark_read {
color: #a81;
}
.flash {
color: green;
background: #efe;
}
.top_menu { .top_menu {
margin-top: 6px; margin-top:6px;
} }
.top_menu a { .top_menu a {
color: blue; color: var(--blue);
} }
.bookmarklet { .bookmarklet {
padding: 1px 2px 0px 2px; padding:1px 2px 0px 2px;
} }
.alert { .alert {
background: #ced; background:var(--reveal-background);
border: 1px solid #acc; border:1px solid var(--border-color);
margin-bottom: 5px;
padding: 2px;
}
.alert.alert-err {
background-color: #ffdfdf
} }
.edit_bookmark_form { .edit_bookmark_form {color:var(--second-foreground);}
color: #888; .edit_bookmark_form input {border:1px solid var(--border-color);}
} .edit_bookmark_form textarea {border:1px solid var(--border-color);}
.edit_bookmark_form input {
border: 1px solid #ddd;
}
.edit_bookmark_form textarea {
border: 1px solid #ddd;
}
.nav-active { .nav-active {
background: #ff8; background-color: var(--reveal-background);
color: blue; color: var(--yellow);
} }
/* mobile device */ /* mobile device */
@media only screen and (max-width: 750px) { @media only screen and (max-width : 750px) {
body { body {
-webkit-text-size-adjust: none; -webkit-text-size-adjust: none;
} }
.display { .display {
float: none; float: none
} }
} }
@media only screen and (max-width: 500px) { @media only screen and (max-width : 500px) {
.filters { .filters {
clear: both; clear: both;
position: relative; position: relative;
@ -197,44 +208,297 @@ label {
} }
.rdim { .rdim {
opacity: 0.8; opacity: .8;
transition: all 0.15s ease-in; transition: all .15s ease-in;
} }
.rdim:hover, .rdim:hover,
.rdim:focus { .rdim:focus {
opacity: 1; opacity: 1;
transition: all 0.15s ease-in; transition: all .15s ease-in;
} }
.display .description > div p, .display .description > div p,
.display .description > div pre { .display .description > div pre
margin-top: 9px; {
margin-bottom: 9px; margin-top: 9px;
margin-bottom: 9px;
} }
.display .description > div > *:first-child { .display .description > div > *:first-child {
margin-top: 2px; margin-top: 2px;
} }
.display .description > div > *:last-child { .display .description > div > *:last-child {
margin-bottom: 2px; margin-bottom: 2px;
} }
.display .description > div > ol li p { .display .description > div > ol li p {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }
.display .description > div > ul li p { .display .description > div > ul li p {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }
.display .description > div ol { .display .description > div ol {
padding-left: 23px; padding-left: 23px;
} }
.display .description > div ul { .display .description > div ul {
padding-left: 23px; padding-left: 23px;
} }
code, code, pre {
pre { font-size:13px;
font-size: 13px;
} }
#content:not([view-rendered]) .view-delay { #content:not([view-rendered]) .view-delay {
display: none !important; display: none !important
} }
/* CSS to be used for HTML Org-mode export
Author: Yann Esposito
*/
:root {
--font-size: 12px;
--line-height: 14px;
}
/* Fonts */
body {
font-family: Menlo, Monaco, monospace;
font-size: var(--font-size);
line-height: var(--line-height);
}
code {
font-family: Menlo, Monaco, monospace;
}
pre, pre code {
font-family: Menlo, Monaco, monospace;
}
.todo, .done {
font-family: Menlo, Monaco, monospace;
}
/* Layout */
body {
margin: 0;
padding: 0;
border: 0;
hyphens: auto;
-webkit-hyphens: auto;
-moz-hyphens:auto;
-ms-hyphens:auto;
}
h1, h2, h3, h4, h5, h6, pre, code, blockquote, ol, ul, ol ol, ul ul, ul ol, ol
ul, li, p, section, header, footer {
float: none;
margin: 0;
padding: 0;
}
h1, h2, h3, h4, h5, h6, pre, code, blockquote, p, ul, ol, section, header,
figure {
margin-top: 1em;
margin-bottom: 1em;
}
figure {
margin: 1em 0px;
}
figure > img {
margin: 0px;
}
li {
position: relative;
display: block;
padding-left: 1.5em;
}
ul > li:before {
content: " ";
opacity: 0.5;
float: left;
position: relative;
left: -1.5em;
text-align: right;
width: 0;
}
ol {
counter-reset: ol;
}
ol > li:before {
content: counter(ol) ". ";
counter-increment: ol;
float: left;
text-align: right;
position: relative;
left: -1.5em;
width: 0;
}
ol > li:nth-child(n+10) {
padding-left:28px;
}
ol > li:nth-child(n+10):before {
left: -28px;
}
img {
max-width: 100%;
max-height: 800px;
margin: 1em auto;
}
p > img, li > img {
max-height: 1em;
margin: 0;
vertical-align: middle;
}
table {
width: 100%;
margin: 1em 0;
border-collapse: collapse;
border: solid 1px;
display: block;
overflow: scroll;
}
td, th {
height: 1em;
padding: 0 10px;
text-align: left;
vertical-align: middle;
border-right: solid 1px;
border-left: solid 1px;
}
/* Markdown tricks */
h1 {
font-size: 1em;
}
h2 {
font-size: 1em;
}
h3 {
font-size: 1em;
}
h4 {
font-size: 1em;
}
h5 {
font-size: 1em;
}
h6 {
font-size: 1em;
}
h1::before {
content: "# ";
}
h2::before {
content: "## ";
}
h3::before {
content: "### ";
}
h4::before {
content: "#### ";
}
h5::before {
content: "##### ";
}
h6::before {
content: "###### ";
}
hr {
position: relative;
height: 1em;
font-size: 0;
line-height: 0;
overflow: hidden;
border: 0;
}
hr:after {
content: "----------------------------------------------------------------------------------------------------";
position: absolute;
top: 0;
left: 0;
width: 100%;
word-wrap: break-word;
}
pre { max-width: 100%; overflow: scroll; }
pre::after,pre::before {
content: "~~~~~~~~~";
display: block; }
pre::before {
content: "~~~~~~~~~ " attr(class);
}
pre code {
background: none;
}
blockquote {
margin-left: 0;
position: relative;
padding-left: 17px;
padding-left: 2ch;
overflow: hidden;
}
.notes *:first-child,
blockquote *:first-child {
margin-top: 0;
}
.notes *:last-child,
blockquote *:last-child {
margin-bottom: 0;
}
blockquote:after {
content: ">\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>";
white-space: pre;
position: absolute;
top: 0;
left: 0;
}
navigation {
font-weight: bold;
display: block;
margin: 10px 0;
}
navigation > a {
margin-right: 10px;
}
/* org mode ids and classes */
.figure {
margin-top: 1em;
margin-bottom: 1em;
}
#content,.content {
margin: 0 1em;
padding: 1px;
}
#content:first-child {
margin-top: 0;
}
.timestamp-wrapper {
font-size: 12px;
}
.todo, .done {
font-size: 12px;
font-weight: bold;
padding: 1px 1ex;
}
.article-date {
font-size: 0.8em;
font-style: italic;
float: right;
}
.footpara {
display: inline;
}
.footdef > sup {
vertical-align: middle;
}
.footdef > sup::after {
content: ": ";
}
.notes {
padding: 5px 10px;
}
.notes::before {
content: "☞";
float: left;
display: inline-block;
width: 1.5em;
}
.underline {
text-decoration: underline;
}
table, tr, td { border-color: transparent; }
.dark-gray { color: var(--second-foreground);}
.mid-gray { color: var(--second-foreground);}

View file

@ -1,58 +1,34 @@
html { html {
box-sizing: border-box; box-sizing: border-box;
} }
[hidden] { [hidden] {
display: none !important; display: none !important
} }
button { button {
background: none; background:none;
border: none; border:none;
padding: 0; padding:0;
cursor: pointer; cursor:pointer;
} }
button:focus { button:focus {
outline: none; outline: none;
} }
input[type="text"], .alert {
input[type="url"], background:#ced;
textarea { border:1px solid #acc;
font-size: 16px;
} }
.alert { form label {
background: #ced;
border: 1px solid #acc;
}
#addForm .alert {
margin-top: -6px;
}
.alert.alert-err {
background-color: #ffdfdf
}
form label {
margin: 0; margin: 0;
vertical-align: middle; vertical-align: middle;
display: table-cell; display: table-cell;
padding: 2px 0; padding: 2px 0;
} }
li { li { list-style-type: none; margin: 0; padding: 0; display: block;}
list-style-type: none;
margin: 0;
padding: 0;
display: block;
}
.when { .when { color:#999}
color: #999; .unread { color:#b41 }
} a.unread { color:#b41 }
.unread { a.bookmark_title { font-size:120%;}
color: #b41;
}
a.unread {
color: #b41;
}
a.bookmark_title {
font-size: 120%;
}
label { label {
cursor: pointer; cursor: pointer;

80
static/js/app.min.js vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -1,38 +1,38 @@
$newline never $newline never
\<!doctype html> \<!doctype html>
\<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]--> \<!--[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 7]> <html class="no-js ie7 oldie" lang="en"> <![endif]-->
\<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en"> <![endif]--> \<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en"> <![endif]-->
\<!--[if gt IE 8]><!--> \<!--[if gt IE 8]><!-->
<html class="no-js" lang="en"> <!--<![endif]--> <html class="no-js" lang="en"> <!--<![endif]-->
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>#{pageTitle pc} <title>#{pageTitle pc}
<meta name="description" content="Espial is an open-source, web-based bookmarking server."> <meta name="description" content="Espial is an open-source, web-based bookmarking server.">
<meta name="robots" content="noindex, nofollow, noodp, noydir"> <meta name="robots" content="noindex, nofollow, noodp, noydir">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
$maybe sourceCodeUri <- msourceCodeUri $maybe sourceCodeUri <- msourceCodeUri
<meta name="source" content="#{ sourceCodeUri }"> <meta name="source" content="#{ sourceCodeUri }">
^{pageHead pc} ^{pageHead pc}
\<!--[if lt IE 9]> \<!--[if lt IE 9]>
\<script src="@{StaticR js_html5shiv_min_js}"></script> \<script src="@{StaticR js_html5shiv_min_js}"></script>
\<![endif]--> \<![endif]-->
<script>document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/, 'js'); <script>document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/, 'js');
<script src="@{StaticR js_js_cookie_2_2_0_min_js}"> <script src="@{StaticR js_js_cookie_2_2_0_min_js}">
<script> <script>
var app = var app =
{ csrfHeaderName: "#{ TE.decodeUtf8 $ CI.foldedCase defaultCsrfHeaderName }" { csrfHeaderName: "#{ TE.decodeUtf8 $ CI.foldedCase defaultCsrfHeaderName }"
, csrfParamName: "#{ defaultCsrfParamName }" , csrfParamName: "#{ defaultCsrfParamName }"
, csrfCookieName: "#{ TE.decodeUtf8 defaultCsrfCookieName }" , csrfCookieName: "#{ TE.decodeUtf8 defaultCsrfCookieName }"
, csrfToken: Cookies.get("#{ TE.decodeUtf8 defaultCsrfCookieName }") , csrfToken: Cookies.get("#{ TE.decodeUtf8 defaultCsrfCookieName }")
, homeR: "@{ HomeR }" , homeR: "@{ HomeR }"
, authRlogoutR: "@{ AuthR LogoutR }" , authRlogoutR: "@{ AuthR LogoutR }"
, userFilterRFilterSingle: "" , userFilterRFilterSingle: ""
, dat: {bmarks : [], bmark: {}, isowner: false, notes: []} , dat: {bmarks : [], bmark: {}, isowner: false, notes: []}
}; };
<body .f6.dark-gray.helvetica> <body .f6.dark-gray.helvetica>
^{pageBody pc} ^{pageBody pc}

View file

@ -14,11 +14,12 @@
<div .top_menu.fr> <div .top_menu.fr>
$maybe userName <- musername $maybe userName <- musername
<a .link href="@?{(AddViewR, [("next","back")])}">add url&nbsp;&nbsp; $maybe currentroute <- mcurrentRoute
<a .link href="@{AddNoteViewR (UserNameP userName)}">add note&nbsp;&nbsp; <a .link href="@?{(AddViewR, [("next",urlrender currentroute)])}">add url&nbsp;&nbsp;
<a .link href="@{NotesR (UserNameP userName)}">notes&nbsp;&nbsp; <a .link href="@{AddNoteViewR (UserNameP userName)}">add note&nbsp;&nbsp;
<a .link href="@{AccountSettingsR}">settings&nbsp;&nbsp; <a .link href="@{NotesR (UserNameP userName)}">notes&nbsp;&nbsp;
<a .link onclick="PS.logoutE(event)()" href="@{AuthR LogoutR}"> <a .link href="@{AccountSettingsR}">settings&nbsp;&nbsp;
<a .link onclick="PS['Main'].logoutE(event)()" href="@{AuthR LogoutR}">
log out log out
$nothing $nothing
<a .link href="@{AuthR LoginR}"> <a .link href="@{AuthR LoginR}">

View file

@ -5,7 +5,7 @@
- (#{userName}) - (#{userName})
<div .fr> <div .fr>
$maybe userName <- musername $maybe userName <- musername
<a .link onclick="PS.logoutE(event)()" href="@{AuthR LogoutR}"> <a .link onclick="PS['Main'].logoutE(event)()" href="@{AuthR LogoutR}">
[log out] [log out]
$nothing $nothing
<a .link href="@{AuthR LoginR}"> <a .link href="@{AuthR LoginR}">

View file

@ -1,7 +1,7 @@
<main #main_column .pv2.ph3.mh1> <main #main_column .pv2.ph3.mh1>
<div .w-100.mw8.center> <div .w-100.mw8.center>
<div .pa3> <div .pa3>
<a .bookmarklet.link.ba.b--dotted.b--light-silver href="javascript:q=location.href;if(document.getSelection){d=document.getSelection();}else{d='';};p=document.title;void(open('@{AddViewR}?_hasdata&url='+encodeURIComponent(q)+'&description='+encodeURIComponent(d)+'&title='+encodeURIComponent(p),'Espial','toolbar=no,width=700,height=360'));">add url bookmarklet <a .bookmarklet.link.ba.b--dotted.b--light-silver href="javascript:q=location.href;if(document.getSelection){d=document.getSelection();}else{d='';};p=document.title;void(open('@{AddViewR}?_hasdata&url='+encodeURIComponent(q)+'&description='+encodeURIComponent(d)+'&title='+encodeURIComponent(p),'Espial','toolbar=no,width=700,height=350'));">add url bookmarklet
<a .pa3 href="@{ChangePasswordR}">Change Password <a .pa3 href="@{ChangePasswordR}">Change Password

View file

@ -1,71 +1,59 @@
$maybe route <- mroute $maybe route <- mroute
<main #main_column .pv2.ph3.mh1> <main #main_column .pv2.ph3.mh1>
<div .w-100.mw8.center> <div .w-100.mw8.center>
<div .fr.nt1 style="margin-bottom:.7rem"> <div .fr.nt1 style="margin-bottom:.7rem">
^{search} ^{search}
<div .di> <div .di>
<div .fl.pr3.dib.mb2> <div .fl.pr3.dib.mb2>
<b> <b>
<a .link href="@{UserR unamep}">#{uname} <a .link href="@{UserR unamep}">#{uname}
$forall tag <- pathtags $forall tag <- pathtags
\ + # \ + #
<a .link href="@{UserTagsR unamep (TagsP [tag])}">#{tag} <a .link href="@{UserTagsR unamep (TagsP [tag])}">#{tag}
<div .fl.pr3.dib.mb2> <div .fl.pr3.dib.mb2>
<span .f7.silver>#{bcount}</span> <span .f7.silver>#{bcount}</span>
$if isowner $if isowner
<div .fl.pr3.dib.mb2> <div .fl.pr3.dib.mb2>
<a .link.silver.hover-blue :isAll:.nav-active <a .link.silver.hover-blue :isAll:.nav-active
href="@{UserR unamep}">all href="@{UserR unamep}">all
<a .link.silver.hover-blue :sharedp == SharedPrivate:.nav-active <a .link.silver.hover-blue :sharedp == SharedPrivate:.nav-active
href="@{UserSharedR unamep SharedPrivate}">private href="@{UserSharedR unamep SharedPrivate}">private
<a .link.silver.hover-blue :sharedp == SharedPublic:.nav-active <a .link.silver.hover-blue :sharedp == SharedPublic:.nav-active
href="@{UserSharedR unamep SharedPublic}">public href="@{UserSharedR unamep SharedPublic}">public
<a .link.silver.hover-blue :filterp == FilterUnread:.nav-active <a .link.silver.hover-blue :filterp == FilterUnread:.nav-active
href="@{UserFilterR unamep FilterUnread}">unread href="@{UserFilterR unamep FilterUnread}">unread
<a .link.silver.hover-blue :filterp == FilterUntagged:.nav-active <a .link.silver.hover-blue :filterp == FilterUntagged:.nav-active
href="@{UserFilterR unamep FilterUntagged}">untagged href="@{UserFilterR unamep FilterUntagged}">untagged
<a .link.silver.hover-blue :filterp == FilterStarred:.nav-active <a .link.silver.hover-blue :filterp == FilterStarred:.nav-active
href="@{UserFilterR unamep FilterStarred}">starred href="@{UserFilterR unamep FilterStarred}">starred
<div .fr.f6.pr3.dib.mb2> <div .fr.f6.pr3.dib.mb2>
$if sharedp == SharedPrivate <a .link.gold.hover-orange
<a .link.gold.hover-orange href="@?{(UserFeedSharedR unamep SharedPrivate, catMaybes [mqueryp])}">RSS href="@{UserFeedR unamep}">RSS
$elseif sharedp == SharedPublic
<a .link.gold.hover-orange href="@?{(UserFeedSharedR unamep SharedPublic, catMaybes [mqueryp])}">RSS <div .cf>
$elseif filterp == FilterUnread
<a .link.gold.hover-orange href="@?{(UserFeedFilterR unamep FilterUnread, catMaybes [mqueryp])}">RSS ^{pager}
$elseif filterp == FilterUntagged
<a .link.gold.hover-orange href="@?{(UserFeedFilterR unamep FilterUntagged, catMaybes [mqueryp])}">RSS <div .cf>
$elseif filterp == FilterStarred
<a .link.gold.hover-orange href="@?{(UserFeedFilterR unamep FilterStarred, catMaybes [mqueryp])}">RSS <div ##{renderEl} .mt3>
$else
<a .link.gold.hover-orange href="@?{(UserFeedR unamep, catMaybes [mqueryp])}">RSS <div .cf>
<div .cf> <div .user_footer.view-delay>
^{pager}
^{pager}
$if (fromIntegral bcount >= limit) || (page > 1)
<div .cf> <div .dib.ml5>
<span .silver.mr1>per page:
<div ##{tagCloudRenderEl}> <a .link.light-silver :limit == 20:.nav-active href="@?{(route, catMaybes [Just ("count", "20"), mqueryp])}"‧>20</a> ‧
<a .link.light-silver :limit == 40:.nav-active href="@?{(route, catMaybes [Just ("count", "40"), mqueryp])}"‧>40</a> ‧
<div ##{renderEl} .mt3> <a .link.light-silver :limit == 80:.nav-active href="@?{(route, catMaybes [Just ("count", "80"), mqueryp])}"‧>80</a> ‧
<a .link.light-silver :limit == 120:.nav-active href="@?{(route, catMaybes [Just ("count", "120"), mqueryp])}"‧>120</a> ‧
<div .cf> <a .link.light-silver :limit == 160:.nav-active href="@?{(route, catMaybes [Just ("count", "160"), mqueryp])}"‧>160</a>
<div .user_footer.view-delay>
^{pager}
$if (fromIntegral bcount >= limit) || (page > 1)
<div .dib.ml5>
<span .silver.mr1>per page:
<a .link.light-silver :limit == 20:.nav-active href="@?{(route, catMaybes [Just ("count", "20"), mqueryp])}"‧>20</a> ‧
<a .link.light-silver :limit == 40:.nav-active href="@?{(route, catMaybes [Just ("count", "40"), mqueryp])}"‧>40</a> ‧
<a .link.light-silver :limit == 80:.nav-active href="@?{(route, catMaybes [Just ("count", "80"), mqueryp])}"‧>80</a> ‧
<a .link.light-silver :limit == 120:.nav-active href="@?{(route, catMaybes [Just ("count", "120"), mqueryp])}"‧>120</a> ‧
<a .link.light-silver :limit == 160:.nav-active href="@?{(route, catMaybes [Just ("count", "160"), mqueryp])}"‧>160</a>

View file

@ -10,7 +10,7 @@ module TestImport
import Application (makeFoundation, makeLogWare) import Application (makeFoundation, makeLogWare)
import ClassyPrelude as X hiding (delete, deleteBy, Handler) import ClassyPrelude as X hiding (delete, deleteBy, Handler)
import Database.Persist as X hiding (get) import Database.Persist as X hiding (get)
import Database.Persist.Sql (SqlPersistM, runSqlPersistMPool, rawExecute, rawSql, unSingle) import Database.Persist.Sql (SqlPersistM, runSqlPersistMPool, rawExecute, rawSql, unSingle, connEscapeName)
import Foundation as X import Foundation as X
import Model as X import Model as X
import Test.Hspec as X import Test.Hspec as X
@ -62,9 +62,8 @@ wipeDB app = do
flip runSqlPersistMPool pool $ do flip runSqlPersistMPool pool $ do
tables <- getTables tables <- getTables
-- sqlBackend <- ask sqlBackend <- ask
-- let queries = map (\t -> "DELETE FROM " ++ (connEscapeName sqlBackend $ DBName t)) tables let queries = map (\t -> "DELETE FROM " ++ (connEscapeName sqlBackend $ DBName t)) tables
let queries = map (\t -> "DELETE FROM " ++ t) tables
forM_ queries (\q -> rawExecute q []) forM_ queries (\q -> rawExecute q [])
getTables :: DB [Text] getTables :: DB [Text]