Compare commits
184 commits
fcec9f3074
...
67962d096d
Author | SHA1 | Date | |
---|---|---|---|
67962d096d | |||
26269d3e54 | |||
983064778d | |||
0e8a60bde7 | |||
0f35911133 | |||
f27d4af0d4 | |||
84e0260396 | |||
824b0f8afd | |||
3d66c508ff | |||
1a3bbef162 | |||
1e695efc87 | |||
2402fde5c3 | |||
62881e3a58 | |||
c1c2aea2da | |||
9e2b767d8d | |||
7319169075 | |||
c209fcf060 | |||
77b0b6d4a0 | |||
5b64e62257 | |||
ff6ffb5688 | |||
c98946ac99 | |||
8fc2433d5d | |||
2e22814241 | |||
89a5cd5775 | |||
d3a7d82dc0 | |||
67bde3b6a3 | |||
5f178e59bd | |||
9682a0c9c1 | |||
e8f423e08d | |||
9e8ec47501 | |||
a9f70eaa88 | |||
f962b947bc | |||
daa7f3d600 | |||
8682c5657e | |||
db53af9f80 | |||
8fc0a0c200 | |||
07fcbb46ad | |||
792be73f72 | |||
30fa32897a | |||
2f7db922fa | |||
3978ac3b71 | |||
c637b56d9b | |||
55fb61d5a0 | |||
02a55aedba | |||
398ab95b34 | |||
b33f377251 | |||
319fe5c636 | |||
7c20823158 | |||
b3737e2191 | |||
6c4266bfc5 | |||
1e596ca4e1 | |||
82cb1fe252 | |||
0514b94e8a | |||
19da371169 | |||
41e1e00b81 | |||
74a0aa682d | |||
9570100c79 | |||
8fa24e5d8e | |||
981b8d4042 | |||
b20913d950 | |||
7d28b2d977 | |||
e45e7bb4c1 | |||
d838344a12 | |||
73123c9b27 | |||
b27c6146cd | |||
c0106544e8 | |||
3c5d30d1fe | |||
9bee6a718b | |||
a0e107e7c0 | |||
d0d53d27f5 | |||
aaad18855b | |||
b0d230edbb | |||
6fa8edbd5e | |||
cbfe99fe00 | |||
edc24a9998 | |||
e7306871cd | |||
745ff90846 | |||
41ee649151 | |||
b6edafbfba | |||
0e255505b5 | |||
804e5000e0 | |||
77b109cf2a | |||
e64d6bf2d5 | |||
6c8661838a | |||
e0ef938565 | |||
bbf0fecf95 | |||
48f0eaf716 | |||
368c5db510 | |||
e7636cc048 | |||
2986230bd5 | |||
750fe0ee52 | |||
70177f9efb | |||
d56da21f40 | |||
5982c88708 | |||
2cf56d9879 | |||
f03c9eb293 | |||
91fa462f8c | |||
3b1c96e08f | |||
ade826d826 | |||
59956c6b59 | |||
b2b19cfaff | |||
0fdae1c935 | |||
ed27a32cff | |||
a080c3017a | |||
ba56d5c429 | |||
cfe85747b6 | |||
2d3b3c3831 | |||
9e53a09304 | |||
8b7ca742b6 | |||
71938b3e0a | |||
3ecb38b89a | |||
db00a1365c | |||
d8f74bc2d6 | |||
d7e72eede3 | |||
c3a126b9ea | |||
ef298cfdd0 | |||
22de2aa78b | |||
feb8920ebe | |||
c7e5d5c3ad | |||
e083a977a8 | |||
6545aaea17 | |||
c98030139b | |||
6550198ab8 | |||
5880b3e5dd | |||
3e7102e2d8 | |||
a0b7c3c782 | |||
24043418ed | |||
df62763440 | |||
c8b999815c | |||
f00c1d778f | |||
5dee5c6856 | |||
7577d3759a | |||
96ffb6e9c6 | |||
cdbb612fd6 | |||
b27e05c635 | |||
b6589d1aa8 | |||
3e380b1dd4 | |||
0b010dfe88 | |||
85fa64979c | |||
89b3bae8d0 | |||
275a42f01a | |||
ef2fd93a66 | |||
6377d229d1 | |||
9c05b86518 | |||
1786cf558e | |||
26d43109dd | |||
559b24a4fc | |||
d770116519 | |||
f6096921f8 | |||
4a574287b9 | |||
7605ddaaa7 | |||
9655253150 | |||
4a3cb641bc | |||
a0b65ca84a | |||
15d12d0494 | |||
a4ce65da2f | |||
eb16eab12e | |||
6277194dff | |||
b136e59265 | |||
6ed148f838 | |||
28ee87890e | |||
ec4e36c36f | |||
136d29d805 | |||
7149aed878 | |||
3aa59305a5 | |||
828a388b90 | |||
91b9d03cc9 | |||
5b1033f63a | |||
8c448e257b | |||
3e6aa226ad | |||
4e182c5afe | |||
0546de274d | |||
01afd5efad | |||
9dabf770a6 | |||
7683c3413b | |||
7df59f4fd8 | |||
1ea0da8f33 | |||
3880c1e303 | |||
f8be202f15 | |||
b0664136e3 | |||
4c272d0198 | |||
ad4bb3fe8f | |||
c0988369a1 | |||
332af3a218 |
36
.github/workflows/tests.yml
vendored
Normal file
36
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
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
2
.gitignore
vendored
|
@ -29,3 +29,5 @@ tmp
|
|||
.cache
|
||||
tags
|
||||
purs/docset/purescript-local.docset/
|
||||
.ghc.environment.x86_64-linux-8.6.5
|
||||
.vscode
|
||||
|
|
24
Makefile
24
Makefile
|
@ -1,3 +1,6 @@
|
|||
_DOCKER:=docker
|
||||
_DOCKER_COMPOSE:=docker compose
|
||||
|
||||
.PHONY: clean build
|
||||
|
||||
all: build
|
||||
|
@ -26,9 +29,8 @@ migrate-createdb:
|
|||
serve:
|
||||
@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}')
|
||||
_EKG_ASSETS_PATH = $$(find ~/.stack -type d | grep ekg.*assets)
|
||||
|
||||
docker-compose-build: build
|
||||
@rm -Rf dist && mkdir -p dist
|
||||
|
@ -36,22 +38,20 @@ docker-compose-build: build
|
|||
@cp -R static dist
|
||||
@rm -Rf dist/static/tmp
|
||||
@cp -R config dist
|
||||
@mkdir -p dist/ekg/assets
|
||||
@cp -R $(_EKG_ASSETS_PATH) dist/ekg
|
||||
@docker-compose build espial
|
||||
@$(_DOCKER_COMPOSE) build espial
|
||||
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-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 espial
|
||||
@$(_DOCKER_COMPOSE) pull espial
|
||||
docker-compose-push:
|
||||
@docker tag localhost/espial:espial $(HUB_REPO)/espial:espial
|
||||
@docker-compose push espial
|
||||
@$(_DOCKER_COMPOSE) push espial
|
||||
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) sh
|
||||
|
||||
|
@ -61,7 +61,7 @@ ifeq ($(_HUB_REPO),)
|
|||
_HUB_REPO := "localhost"
|
||||
endif
|
||||
|
||||
docker_espial = docker-compose exec espial
|
||||
docker_espial = $(_DOCKER_COMPOSE) exec espial
|
||||
|
||||
clean:
|
||||
@stack clean
|
||||
|
|
33
README.md
33
README.md
|
@ -8,13 +8,15 @@ 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.
|
||||
|
||||
Also, see the android app for adding bookmarks via an Android Share intent https://github.com/jonschoning/espial-share-android
|
||||
|
||||
## demo server
|
||||
|
||||
log in — username: demo password: demo
|
||||
|
||||
https://esp.ae8.org/u:demo
|
||||
|
||||
![jpg](https://i.imgur.com/XikHLua.png)
|
||||
![jpg](https://i.imgur.com/jdnV93c.png)
|
||||
|
||||
## Docker Setup
|
||||
|
||||
|
@ -43,25 +45,36 @@ see https://github.com/jonschoning/espial-docker
|
|||
stack exec migration -- createuser --conn espial.sqlite3 --userName myusername --userPassword myuserpassword
|
||||
```
|
||||
|
||||
5. Import a bookmark file for a user (optional)
|
||||
5. Import a pinboard 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. Start a production server:
|
||||
6. Import a firefox bookmark file for a user (optional)
|
||||
|
||||
```
|
||||
stack exec espial -- +RTS -T
|
||||
stack exec migration -- importfirefoxbookmarks --conn espial.sqlite3 --userName myusername --bookmarkFile firefox-bookmarks.json
|
||||
```
|
||||
|
||||
see `config/settings.yml` for changing default run-time parameters / environment variables
|
||||
7. Start a production server:
|
||||
|
||||
default app http port: `3000`
|
||||
```
|
||||
stack exec espial
|
||||
```
|
||||
|
||||
default ekg http port: `8000`
|
||||
### Configuration
|
||||
|
||||
ssl: use reverse proxy
|
||||
See `config/settings.yml` for changing default run-time parameters & environment variables.
|
||||
- `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
|
||||
|
||||
|
@ -80,7 +93,7 @@ ssl: use reverse proxy
|
|||
|
||||
- See `purs/` folder
|
||||
|
||||
## Import Bookmark file format
|
||||
## Import Bookmark file format (pinboard compatible format)
|
||||
|
||||
see `sample-bookmarks.json`, which contains a JSON array, each line containing a `FileBookmark` object.
|
||||
|
||||
|
|
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report vulnerabilities to jonschoning@gmail.com
|
|
@ -12,25 +12,64 @@ import ClassyPrelude
|
|||
import Lens.Micro
|
||||
|
||||
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
|
||||
= CreateDB { conn :: Text}
|
||||
= CreateDB { conn :: Text }
|
||||
| CreateUser { conn :: Text
|
||||
, userName :: Text
|
||||
, userPassword :: Text
|
||||
, userApiToken :: Maybe Text }
|
||||
, userPassword :: Password
|
||||
, privateDefault :: Maybe Bool
|
||||
, archiveDefault :: Maybe Bool
|
||||
, privacyLock :: Maybe Bool }
|
||||
| CreateApiKey { conn :: Text
|
||||
, userName :: Text }
|
||||
| DeleteUser { conn :: Text
|
||||
, userName :: Text}
|
||||
, userName :: Text }
|
||||
| DeleteApiKey { conn :: Text
|
||||
, userName :: Text }
|
||||
| ImportBookmarks { conn :: Text
|
||||
, userName :: Text
|
||||
, bookmarkFile :: FilePath}
|
||||
, bookmarkFile :: FilePath }
|
||||
| ImportFirefoxBookmarks { conn :: Text
|
||||
, userName :: Text
|
||||
, bookmarkFile :: FilePath }
|
||||
| ExportBookmarks { conn :: Text
|
||||
, userName :: Text
|
||||
, bookmarkFile :: FilePath}
|
||||
, bookmarkFile :: FilePath }
|
||||
| ImportNotes { conn :: Text
|
||||
, userName :: Text
|
||||
, noteDirectory :: FilePath}
|
||||
| PrintMigrateDB { conn :: Text}
|
||||
, noteDirectory :: FilePath }
|
||||
| PrintMigrateDB { conn :: Text }
|
||||
deriving (Generic, Show)
|
||||
|
||||
instance ParseRecord MigrationOpts
|
||||
|
@ -39,54 +78,97 @@ main :: IO ()
|
|||
main = do
|
||||
args <- getRecord "Migrations"
|
||||
case args of
|
||||
PrintMigrateDB conn ->
|
||||
PrintMigrateDB {..} ->
|
||||
P.runSqlite conn dumpMigration
|
||||
|
||||
CreateDB conn -> do
|
||||
CreateDB {..} -> do
|
||||
let connInfo = P.mkSqliteConnectionInfo conn
|
||||
& set P.fkEnabled False
|
||||
P.runSqliteInfo connInfo runMigrations
|
||||
|
||||
CreateUser conn uname upass utoken ->
|
||||
CreateUser{..} ->
|
||||
P.runSqlite conn $ do
|
||||
hash' <- liftIO (hashPassword upass)
|
||||
passwordText <- liftIO . fmap T.strip $ case userPassword of
|
||||
PasswordText s -> pure s
|
||||
PasswordFile f -> readFileUtf8 f
|
||||
hash' <- liftIO (hashPassword passwordText)
|
||||
void $ P.upsertBy
|
||||
(UniqueUserName uname)
|
||||
(User uname hash' utoken False False False)
|
||||
(UniqueUserName userName)
|
||||
(User userName hash' Nothing False False False)
|
||||
[ UserPasswordHash P.=. hash'
|
||||
, UserApiToken P.=. utoken
|
||||
, UserPrivateDefault P.=. False
|
||||
, UserArchiveDefault P.=. False
|
||||
, UserPrivacyLock P.=. False
|
||||
, UserPrivateDefault P.=. fromMaybe False privateDefault
|
||||
, UserArchiveDefault P.=. fromMaybe False archiveDefault
|
||||
, UserPrivacyLock P.=. fromMaybe False privacyLock
|
||||
]
|
||||
pure () :: DB ()
|
||||
|
||||
DeleteUser conn uname ->
|
||||
CreateApiKey {..} ->
|
||||
P.runSqlite conn $ do
|
||||
muser <- P.getBy (UniqueUserName uname)
|
||||
apiKey@(ApiKey plainKey) <- liftIO generateApiKey
|
||||
muser <- P.getBy (UniqueUserName userName)
|
||||
case muser of
|
||||
Nothing -> liftIO (print (uname ++ "not found"))
|
||||
Nothing -> liftIO (print (userName ++ " not found"))
|
||||
Just (P.Entity uid _) -> do
|
||||
P.deleteCascade uid
|
||||
-- API key is only displayed once after creation,
|
||||
-- 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 ()
|
||||
|
||||
ImportBookmarks conn uname file ->
|
||||
ExportBookmarks {..} ->
|
||||
P.runSqlite conn $ do
|
||||
muser <- P.getBy (UniqueUserName uname)
|
||||
muser <- P.getBy (UniqueUserName userName)
|
||||
case muser of
|
||||
Just (P.Entity uid _) -> insertFileBookmarks uid file
|
||||
Nothing -> liftIO (print (uname ++ "not found"))
|
||||
Just (P.Entity uid _) -> exportFileBookmarks uid bookmarkFile
|
||||
Nothing -> liftIO (print (userName ++ "not found"))
|
||||
|
||||
ExportBookmarks conn uname file ->
|
||||
ImportBookmarks {..} ->
|
||||
P.runSqlite conn $ do
|
||||
muser <- P.getBy (UniqueUserName uname)
|
||||
muser <- P.getBy (UniqueUserName userName)
|
||||
case muser of
|
||||
Just (P.Entity uid _) -> exportFileBookmarks uid file
|
||||
Nothing -> liftIO (print (uname ++ "not found"))
|
||||
Just (P.Entity uid _) -> do
|
||||
result <- insertFileBookmarks uid bookmarkFile
|
||||
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
|
||||
muser <- P.getBy (UniqueUserName uname)
|
||||
muser <- P.getBy (UniqueUserName userName)
|
||||
case muser of
|
||||
Just (P.Entity uid _) -> insertDirFileNotes uid dir
|
||||
Nothing -> liftIO (print (uname ++ "not found"))
|
||||
Just (P.Entity uid _) -> do
|
||||
result <- insertFFBookmarks uid bookmarkFile
|
||||
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"))
|
||||
|
|
22
changelog.md
22
changelog.md
|
@ -1,3 +1,25 @@
|
|||
__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__
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
-- notes
|
||||
!/#UserNameP/notes NotesR GET
|
||||
!/#UserNameP/notes/add AddNoteViewR GET
|
||||
!/notes/add AddNoteSlimViewR GET
|
||||
!/#UserNameP/notes/feed.xml NotesFeedR GET
|
||||
!/#UserNameP/notes/#NtSlug NoteR GET
|
||||
!/api/note/add AddNoteR POST
|
||||
|
@ -19,7 +20,11 @@
|
|||
!/#UserNameP/#SharedP UserSharedR GET
|
||||
!/#UserNameP/#FilterP UserFilterR GET
|
||||
!/#UserNameP/#TagsP UserTagsR 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 AccountSettingsR GET
|
||||
|
@ -32,6 +37,11 @@ api/accountSettings EditAccountSettingsR POST
|
|||
/add AddViewR GET
|
||||
api/add AddR POST
|
||||
|
||||
-- api
|
||||
api/lookuptitle LookupTitleR POST
|
||||
api/tagcloud UserTagCloudR POST
|
||||
api/tagcloudmode UserTagCloudModeR POST
|
||||
|
||||
-- edit
|
||||
/bm/#Int64 DeleteR DELETE
|
||||
/bm/#Int64/read ReadR POST
|
||||
|
@ -39,4 +49,4 @@ api/add AddR POST
|
|||
/bm/#Int64/unstar UnstarR POST
|
||||
|
||||
-- doc
|
||||
/docs/search DocsSearchR GET
|
||||
/docs/search DocsSearchR GET
|
||||
|
|
|
@ -18,8 +18,8 @@ ip-from-header: "_env:IP_FROM_HEADER:false"
|
|||
# Optional values with the following production defaults.
|
||||
# In development, they default to the inverse.
|
||||
#
|
||||
# detailed-logging: false
|
||||
# should-log-all: false
|
||||
detailed-logging: "_env:DETAILED_LOGGING" # false
|
||||
should-log-all: "_env:SHOULD_LOG_ALL" # false
|
||||
# reload-templates: false
|
||||
# mutable-static: false
|
||||
# skip-combining: false
|
||||
|
@ -37,7 +37,11 @@ database:
|
|||
copyright: Insert copyright statement here
|
||||
#analytics: UA-YOURCODE
|
||||
|
||||
ekg-host: "_env:EKG_HOST:0.0.0.0"
|
||||
ekg-port: "_env:EKG_PORT:8000"
|
||||
archive-socks-proxy-host: "_env:ARCHIVE_SOCKS_PROXY_HOST"
|
||||
archive-socks-proxy-port: "_env:ARCHIVE_SOCKS_PROXY_PORT"
|
||||
|
||||
source-code-uri: "https://github.com/jonschoning/espial"
|
||||
source-code-uri: "_env: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"
|
||||
|
|
|
@ -7,10 +7,16 @@ services:
|
|||
dockerfile: ../Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- '$APPDATA:/app/data'
|
||||
network_mode: host
|
||||
environment:
|
||||
- IP_FROM_HEADER=true
|
||||
- SQLITE_DATABASE=/app/data/espial.sqlite3
|
||||
- ekg_datadir=ekg
|
||||
# - SSL_ONLY=false
|
||||
# - 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
|
||||
|
|
339
espial.cabal
339
espial.cabal
|
@ -1,13 +1,11 @@
|
|||
cabal-version: 1.12
|
||||
|
||||
-- This file has been generated from package.yaml by hpack version 0.31.2.
|
||||
-- This file has been generated from package.yaml by hpack version 0.35.0.
|
||||
--
|
||||
-- see: https://github.com/sol/hpack
|
||||
--
|
||||
-- hash: da944088abb7ae887d67efd710c100bdbd5587072c6ddcfdc5d05392e7509d85
|
||||
|
||||
name: espial
|
||||
version: 0.0.8
|
||||
version: 0.0.15
|
||||
synopsis: Espial is an open-source, web-based bookmarking server.
|
||||
description: .
|
||||
Espial is an open-source, web-based bookmarking server.
|
||||
|
@ -64,7 +62,6 @@ extra-source-files:
|
|||
purs/src/App.purs
|
||||
purs/src/Globals.js
|
||||
purs/src/Globals.purs
|
||||
purs/src/Main.js
|
||||
purs/src/Main.purs
|
||||
purs/src/Marked.js
|
||||
purs/src/Marked.purs
|
||||
|
@ -79,6 +76,7 @@ extra-source-files:
|
|||
purs/src/Component/NNote.purs
|
||||
purs/src/Component/RawHtml.js
|
||||
purs/src/Component/RawHtml.purs
|
||||
purs/src/Component/TagCloud.purs
|
||||
purs/test/Main.purs
|
||||
|
||||
source-repository head
|
||||
|
@ -122,48 +120,86 @@ library
|
|||
Paths_espial
|
||||
hs-source-dirs:
|
||||
src
|
||||
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
|
||||
default-extensions:
|
||||
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:
|
||||
aeson >=1.4
|
||||
, attoparsec
|
||||
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
|
||||
, base64
|
||||
, bcrypt >=0.0.8
|
||||
, blaze-html >=0.9 && <1.0
|
||||
, bytestring >=0.9 && <0.11
|
||||
, bytestring >=0.9 && <0.14
|
||||
, case-insensitive
|
||||
, classy-prelude >=1.4 && <1.6
|
||||
, classy-prelude-conduit >=1.4 && <1.6
|
||||
, classy-prelude-yesod >=1.4 && <1.6
|
||||
, conduit >=1.0 && <2.0
|
||||
, connection
|
||||
, containers
|
||||
, cryptohash-sha256
|
||||
, data-default
|
||||
, directory >=1.1 && <1.4
|
||||
, ekg
|
||||
, ekg-core
|
||||
, entropy
|
||||
, esqueleto
|
||||
, fast-logger >=2.2 && <2.5
|
||||
, fast-logger >=2.2 && <4
|
||||
, file-embed
|
||||
, foreign-store
|
||||
, hjsmin >=0.1 && <0.3
|
||||
, hscolour
|
||||
, html-entities
|
||||
, http-api-data >=0.3.4
|
||||
, http-client
|
||||
, http-client-tls >=0.3 && <0.4
|
||||
, http-conduit >=2.3 && <2.4
|
||||
, http-client-tls ==0.3.*
|
||||
, http-conduit ==2.3.*
|
||||
, http-types
|
||||
, iso8601-time >=0.1.3
|
||||
, microlens
|
||||
, monad-logger >=0.3 && <0.4
|
||||
, monad-metrics
|
||||
, monad-logger ==0.3.*
|
||||
, mtl
|
||||
, parser-combinators
|
||||
, persistent >=2.8 && <2.10
|
||||
, persistent >=2.8 && <2.14
|
||||
, persistent-sqlite >=2.6.2
|
||||
, persistent-template >=2.5 && <2.9
|
||||
, persistent-template >=2.5 && <2.13
|
||||
, pretty-show
|
||||
, safe
|
||||
, shakespeare >=2.0 && <2.1
|
||||
, shakespeare ==2.0.*
|
||||
, template-haskell
|
||||
, text >=0.11 && <2.0
|
||||
, time
|
||||
|
@ -171,23 +207,22 @@ library
|
|||
, unordered-containers
|
||||
, vector
|
||||
, wai
|
||||
, wai-extra >=3.0 && <3.1
|
||||
, wai-logger >=2.2 && <2.4
|
||||
, wai-middleware-metrics
|
||||
, warp >=3.0 && <3.3
|
||||
, wai-extra >=3.0 && <3.2
|
||||
, wai-logger
|
||||
, warp >=3.0 && <3.4
|
||||
, yaml >=0.8 && <0.12
|
||||
, yesod >=1.6 && <1.7
|
||||
, yesod-auth >=1.6 && <1.7
|
||||
, yesod-core >=1.6 && <1.7
|
||||
, yesod-form >=1.6 && <1.7
|
||||
, yesod-newsfeed >=1.6 && <1.7
|
||||
, yesod-static >=1.6 && <1.7
|
||||
, yesod >=1.6 && <1.8
|
||||
, yesod-auth >=1.6 && <1.8
|
||||
, yesod-core >=1.6 && <1.8
|
||||
, yesod-form >=1.6 && <1.8
|
||||
, yesod-newsfeed >=1.6 && <1.8
|
||||
, yesod-static >=1.6 && <1.8
|
||||
default-language: Haskell2010
|
||||
if (flag(dev)) || (flag(library-only))
|
||||
ghc-options: -Wall -fwarn-tabs -O0
|
||||
cpp-options: -DDEVELOPMENT
|
||||
else
|
||||
ghc-options: -Wall -fwarn-tabs -O2
|
||||
default-language: Haskell2010
|
||||
|
||||
executable espial
|
||||
main-is: main.hs
|
||||
|
@ -196,50 +231,88 @@ executable espial
|
|||
Paths_espial
|
||||
hs-source-dirs:
|
||||
app
|
||||
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
|
||||
default-extensions:
|
||||
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
|
||||
build-depends:
|
||||
aeson >=1.4
|
||||
, attoparsec
|
||||
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
|
||||
, base64
|
||||
, bcrypt >=0.0.8
|
||||
, blaze-html >=0.9 && <1.0
|
||||
, bytestring >=0.9 && <0.11
|
||||
, bytestring >=0.9 && <0.14
|
||||
, case-insensitive
|
||||
, classy-prelude >=1.4 && <1.6
|
||||
, classy-prelude-conduit >=1.4 && <1.6
|
||||
, classy-prelude-yesod >=1.4 && <1.6
|
||||
, conduit >=1.0 && <2.0
|
||||
, connection
|
||||
, containers
|
||||
, cryptohash-sha256
|
||||
, data-default
|
||||
, directory >=1.1 && <1.4
|
||||
, ekg
|
||||
, ekg-core
|
||||
, entropy
|
||||
, espial
|
||||
, esqueleto
|
||||
, fast-logger >=2.2 && <2.5
|
||||
, fast-logger >=2.2 && <4
|
||||
, file-embed
|
||||
, foreign-store
|
||||
, hjsmin >=0.1 && <0.3
|
||||
, hscolour
|
||||
, html-entities
|
||||
, http-api-data >=0.3.4
|
||||
, http-client
|
||||
, http-client-tls >=0.3 && <0.4
|
||||
, http-conduit >=2.3 && <2.4
|
||||
, http-client-tls ==0.3.*
|
||||
, http-conduit ==2.3.*
|
||||
, http-types
|
||||
, iso8601-time >=0.1.3
|
||||
, microlens
|
||||
, monad-logger >=0.3 && <0.4
|
||||
, monad-metrics
|
||||
, monad-logger ==0.3.*
|
||||
, mtl
|
||||
, parser-combinators
|
||||
, persistent >=2.8 && <2.10
|
||||
, persistent >=2.8 && <2.14
|
||||
, persistent-sqlite >=2.6.2
|
||||
, persistent-template >=2.5 && <2.9
|
||||
, persistent-template >=2.5 && <2.13
|
||||
, pretty-show
|
||||
, safe
|
||||
, shakespeare >=2.0 && <2.1
|
||||
, shakespeare ==2.0.*
|
||||
, template-haskell
|
||||
, text >=0.11 && <2.0
|
||||
, time
|
||||
|
@ -247,20 +320,19 @@ executable espial
|
|||
, unordered-containers
|
||||
, vector
|
||||
, wai
|
||||
, wai-extra >=3.0 && <3.1
|
||||
, wai-logger >=2.2 && <2.4
|
||||
, wai-middleware-metrics
|
||||
, warp >=3.0 && <3.3
|
||||
, wai-extra >=3.0 && <3.2
|
||||
, wai-logger
|
||||
, warp >=3.0 && <3.4
|
||||
, yaml >=0.8 && <0.12
|
||||
, yesod >=1.6 && <1.7
|
||||
, yesod-auth >=1.6 && <1.7
|
||||
, yesod-core >=1.6 && <1.7
|
||||
, yesod-form >=1.6 && <1.7
|
||||
, yesod-newsfeed >=1.6 && <1.7
|
||||
, yesod-static >=1.6 && <1.7
|
||||
, yesod >=1.6 && <1.8
|
||||
, yesod-auth >=1.6 && <1.8
|
||||
, yesod-core >=1.6 && <1.8
|
||||
, yesod-form >=1.6 && <1.8
|
||||
, yesod-newsfeed >=1.6 && <1.8
|
||||
, yesod-static >=1.6 && <1.8
|
||||
default-language: Haskell2010
|
||||
if flag(library-only)
|
||||
buildable: False
|
||||
default-language: Haskell2010
|
||||
|
||||
executable migration
|
||||
main-is: Main.hs
|
||||
|
@ -268,51 +340,90 @@ executable migration
|
|||
Paths_espial
|
||||
hs-source-dirs:
|
||||
app/migration
|
||||
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
|
||||
default-extensions:
|
||||
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
|
||||
build-depends:
|
||||
aeson >=1.4
|
||||
, attoparsec
|
||||
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
|
||||
, base64
|
||||
, bcrypt >=0.0.8
|
||||
, blaze-html >=0.9 && <1.0
|
||||
, bytestring >=0.9 && <0.11
|
||||
, bytestring >=0.9 && <0.14
|
||||
, case-insensitive
|
||||
, classy-prelude >=1.4 && <1.6
|
||||
, classy-prelude-conduit >=1.4 && <1.6
|
||||
, classy-prelude-yesod >=1.4 && <1.6
|
||||
, conduit >=1.0 && <2.0
|
||||
, connection
|
||||
, containers
|
||||
, cryptohash-sha256
|
||||
, data-default
|
||||
, directory >=1.1 && <1.4
|
||||
, ekg
|
||||
, ekg-core
|
||||
, entropy
|
||||
, espial
|
||||
, esqueleto
|
||||
, fast-logger >=2.2 && <2.5
|
||||
, fast-logger >=2.2 && <4
|
||||
, file-embed
|
||||
, foreign-store
|
||||
, hjsmin >=0.1 && <0.3
|
||||
, hscolour
|
||||
, html-entities
|
||||
, http-api-data >=0.3.4
|
||||
, http-client
|
||||
, http-client-tls >=0.3 && <0.4
|
||||
, http-conduit >=2.3 && <2.4
|
||||
, http-client-tls ==0.3.*
|
||||
, http-conduit ==2.3.*
|
||||
, http-types
|
||||
, iso8601-time >=0.1.3
|
||||
, microlens
|
||||
, monad-logger >=0.3 && <0.4
|
||||
, monad-metrics
|
||||
, monad-logger ==0.3.*
|
||||
, mtl
|
||||
, optparse-applicative
|
||||
, optparse-generic >=1.2.3
|
||||
, parser-combinators
|
||||
, persistent >=2.8 && <2.10
|
||||
, persistent >=2.8 && <2.14
|
||||
, persistent-sqlite >=2.6.2
|
||||
, persistent-template >=2.5 && <2.9
|
||||
, persistent-template >=2.5 && <2.13
|
||||
, pretty-show
|
||||
, safe
|
||||
, shakespeare >=2.0 && <2.1
|
||||
, shakespeare ==2.0.*
|
||||
, template-haskell
|
||||
, text >=0.11 && <2.0
|
||||
, time
|
||||
|
@ -320,20 +431,19 @@ executable migration
|
|||
, unordered-containers
|
||||
, vector
|
||||
, wai
|
||||
, wai-extra >=3.0 && <3.1
|
||||
, wai-logger >=2.2 && <2.4
|
||||
, wai-middleware-metrics
|
||||
, warp >=3.0 && <3.3
|
||||
, wai-extra >=3.0 && <3.2
|
||||
, wai-logger
|
||||
, warp >=3.0 && <3.4
|
||||
, yaml >=0.8 && <0.12
|
||||
, yesod >=1.6 && <1.7
|
||||
, yesod-auth >=1.6 && <1.7
|
||||
, yesod-core >=1.6 && <1.7
|
||||
, yesod-form >=1.6 && <1.7
|
||||
, yesod-newsfeed >=1.6 && <1.7
|
||||
, yesod-static >=1.6 && <1.7
|
||||
, yesod >=1.6 && <1.8
|
||||
, yesod-auth >=1.6 && <1.8
|
||||
, yesod-core >=1.6 && <1.8
|
||||
, yesod-form >=1.6 && <1.8
|
||||
, yesod-newsfeed >=1.6 && <1.8
|
||||
, yesod-static >=1.6 && <1.8
|
||||
default-language: Haskell2010
|
||||
if flag(library-only)
|
||||
buildable: False
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite test
|
||||
type: exitcode-stdio-1.0
|
||||
|
@ -345,51 +455,89 @@ test-suite test
|
|||
Paths_espial
|
||||
hs-source-dirs:
|
||||
test
|
||||
default-extensions: BangPatterns CPP ConstraintKinds DataKinds DeriveDataTypeable DeriveGeneric EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NoImplicitPrelude OverloadedStrings PolyKinds PolymorphicComponents QuasiQuotes Rank2Types RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators TypeSynonymInstances ViewPatterns
|
||||
default-extensions:
|
||||
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
|
||||
build-depends:
|
||||
aeson >=1.4
|
||||
, attoparsec
|
||||
, base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
|
||||
, base64
|
||||
, bcrypt >=0.0.8
|
||||
, blaze-html >=0.9 && <1.0
|
||||
, bytestring >=0.9 && <0.11
|
||||
, bytestring >=0.9 && <0.14
|
||||
, case-insensitive
|
||||
, classy-prelude >=1.4 && <1.6
|
||||
, classy-prelude-conduit >=1.4 && <1.6
|
||||
, classy-prelude-yesod >=1.4 && <1.6
|
||||
, conduit >=1.0 && <2.0
|
||||
, connection
|
||||
, containers
|
||||
, cryptohash-sha256
|
||||
, data-default
|
||||
, directory >=1.1 && <1.4
|
||||
, ekg
|
||||
, ekg-core
|
||||
, entropy
|
||||
, espial
|
||||
, esqueleto
|
||||
, fast-logger >=2.2 && <2.5
|
||||
, fast-logger >=2.2 && <4
|
||||
, file-embed
|
||||
, foreign-store
|
||||
, hjsmin >=0.1 && <0.3
|
||||
, hscolour
|
||||
, hspec >=2.0.0
|
||||
, html-entities
|
||||
, http-api-data >=0.3.4
|
||||
, http-client
|
||||
, http-client-tls >=0.3 && <0.4
|
||||
, http-conduit >=2.3 && <2.4
|
||||
, http-client-tls ==0.3.*
|
||||
, http-conduit ==2.3.*
|
||||
, http-types
|
||||
, iso8601-time >=0.1.3
|
||||
, microlens
|
||||
, monad-logger >=0.3 && <0.4
|
||||
, monad-metrics
|
||||
, monad-logger ==0.3.*
|
||||
, mtl
|
||||
, parser-combinators
|
||||
, persistent >=2.8 && <2.10
|
||||
, persistent >=2.8 && <2.14
|
||||
, persistent-sqlite >=2.6.2
|
||||
, persistent-template >=2.5 && <2.9
|
||||
, persistent-template >=2.5 && <2.13
|
||||
, pretty-show
|
||||
, safe
|
||||
, shakespeare >=2.0 && <2.1
|
||||
, shakespeare ==2.0.*
|
||||
, template-haskell
|
||||
, text >=0.11 && <2.0
|
||||
, time
|
||||
|
@ -397,16 +545,15 @@ test-suite test
|
|||
, unordered-containers
|
||||
, vector
|
||||
, wai
|
||||
, wai-extra >=3.0 && <3.1
|
||||
, wai-logger >=2.2 && <2.4
|
||||
, wai-middleware-metrics
|
||||
, warp >=3.0 && <3.3
|
||||
, wai-extra >=3.0 && <3.2
|
||||
, wai-logger
|
||||
, warp >=3.0 && <3.4
|
||||
, yaml >=0.8 && <0.12
|
||||
, yesod >=1.6 && <1.7
|
||||
, yesod-auth >=1.6 && <1.7
|
||||
, yesod-core >=1.6 && <1.7
|
||||
, yesod-form >=1.6 && <1.7
|
||||
, yesod-newsfeed >=1.6 && <1.7
|
||||
, yesod-static >=1.6 && <1.7
|
||||
, yesod >=1.6 && <1.8
|
||||
, yesod-auth >=1.6 && <1.8
|
||||
, yesod-core >=1.6 && <1.8
|
||||
, yesod-form >=1.6 && <1.8
|
||||
, yesod-newsfeed >=1.6 && <1.8
|
||||
, yesod-static >=1.6 && <1.8
|
||||
, yesod-test
|
||||
default-language: Haskell2010
|
||||
|
|
11
espial.code-workspace
Normal file
11
espial.code-workspace
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "purs"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
44
package.yaml
44
package.yaml
|
@ -1,6 +1,6 @@
|
|||
name: espial
|
||||
synopsis: Espial is an open-source, web-based bookmarking server.
|
||||
version: "0.0.8"
|
||||
version: "0.0.15"
|
||||
description: ! '
|
||||
|
||||
Espial is an open-source, web-based bookmarking server.
|
||||
|
@ -42,11 +42,13 @@ extra-source-files:
|
|||
|
||||
default-extensions:
|
||||
- BangPatterns
|
||||
- BlockArguments
|
||||
- CPP
|
||||
- ConstraintKinds
|
||||
- DataKinds
|
||||
- DeriveDataTypeable
|
||||
- DeriveGeneric
|
||||
- DerivingStrategies
|
||||
- EmptyDataDecls
|
||||
- FlexibleContexts
|
||||
- FlexibleInstances
|
||||
|
@ -61,6 +63,7 @@ default-extensions:
|
|||
- OverloadedStrings
|
||||
- PolyKinds
|
||||
- PolymorphicComponents
|
||||
- PartialTypeSignatures
|
||||
- QuasiQuotes
|
||||
- Rank2Types
|
||||
- RankNTypes
|
||||
|
@ -73,6 +76,7 @@ default-extensions:
|
|||
- TypeFamilies
|
||||
- TypeOperators
|
||||
- TypeSynonymInstances
|
||||
- UndecidableInstances
|
||||
- ViewPatterns
|
||||
|
||||
dependencies:
|
||||
|
@ -81,37 +85,34 @@ dependencies:
|
|||
# See: https://ghc.haskell.org/trac/ghc/ticket/12130
|
||||
- base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
|
||||
|
||||
- yesod >=1.6 && <1.7
|
||||
- yesod-core >=1.6 && <1.7
|
||||
- yesod-auth >=1.6 && <1.7
|
||||
- yesod-static >=1.6 && <1.7
|
||||
- yesod-form >=1.6 && <1.7
|
||||
- yesod-newsfeed >= 1.6 && < 1.7
|
||||
- yesod >=1.6 && <1.8
|
||||
- yesod-core >=1.6 && <1.8
|
||||
- yesod-auth >=1.6 && <1.8
|
||||
- yesod-static >=1.6 && <1.8
|
||||
- yesod-form >=1.6 && <1.8
|
||||
- yesod-newsfeed >= 1.6 && < 1.8
|
||||
- classy-prelude >=1.4 && <1.6
|
||||
- classy-prelude-conduit >=1.4 && <1.6
|
||||
- classy-prelude-yesod >=1.4 && <1.6
|
||||
- bytestring >=0.9 && <0.11
|
||||
- bytestring >=0.9 && <0.14
|
||||
- text >=0.11 && <2.0
|
||||
- persistent >=2.8 && <2.10
|
||||
# - persistent-postgresql >=2.8 && <2.9
|
||||
- persistent >=2.8 && <2.14
|
||||
- blaze-html >= 0.9 && < 1.0
|
||||
- persistent-template >=2.5 && <2.9
|
||||
- persistent-template >=2.5 && <2.13
|
||||
- template-haskell
|
||||
- shakespeare >=2.0 && <2.1
|
||||
- hjsmin >=0.1 && <0.3
|
||||
# - monad-control >=0.3 && <1.1
|
||||
- wai-extra >=3.0 && <3.1
|
||||
- wai-extra >=3.0 && <3.2
|
||||
- yaml >=0.8 && <0.12
|
||||
- http-client-tls >=0.3 && <0.4
|
||||
- http-conduit >=2.3 && <2.4
|
||||
- directory >=1.1 && <1.4
|
||||
- warp >=3.0 && <3.3
|
||||
- warp >=3.0 && <3.4
|
||||
- data-default
|
||||
# - aeson >=0.6 && <1.4
|
||||
- conduit >=1.0 && <2.0
|
||||
- monad-logger >=0.3 && <0.4
|
||||
- fast-logger >=2.2 && <2.5
|
||||
- wai-logger >=2.2 && <2.4
|
||||
- fast-logger >=2.2 && <4
|
||||
- wai-logger
|
||||
- file-embed
|
||||
- safe
|
||||
- unordered-containers
|
||||
|
@ -126,8 +127,6 @@ dependencies:
|
|||
- attoparsec
|
||||
- bcrypt >= 0.0.8
|
||||
- entropy
|
||||
- ekg
|
||||
- ekg-core
|
||||
- esqueleto
|
||||
- hscolour
|
||||
- http-api-data >= 0.3.4
|
||||
|
@ -135,13 +134,15 @@ dependencies:
|
|||
- http-types
|
||||
- iso8601-time >=0.1.3
|
||||
- microlens
|
||||
- monad-metrics
|
||||
- mtl
|
||||
- persistent-sqlite >=2.6.2
|
||||
- pretty-show
|
||||
- transformers >= 0.2.2
|
||||
- wai-middleware-metrics
|
||||
- parser-combinators
|
||||
- html-entities
|
||||
- connection
|
||||
- base64
|
||||
- cryptohash-sha256
|
||||
|
||||
# The library contains all of our application code. The executable
|
||||
# defined below is just a thin wrapper.
|
||||
|
@ -186,6 +187,7 @@ executables:
|
|||
dependencies:
|
||||
- espial
|
||||
- optparse-generic >= 1.2.3
|
||||
- optparse-applicative
|
||||
|
||||
# Test suite
|
||||
tests:
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
.PHONY: clean build
|
||||
|
||||
all: build
|
||||
all: bundle
|
||||
|
||||
install:
|
||||
spago install
|
||||
@npm run spago install
|
||||
|
||||
build:
|
||||
@spago build
|
||||
@spago bundle-app --to dist/bundle.js
|
||||
@npm run parcel-build
|
||||
@npm run spago build
|
||||
|
||||
bundle: build
|
||||
@npm run esbuild-bundle
|
||||
@npm run esbuild-bundle-min
|
||||
@rm -f dist/*.gz
|
||||
@gzip -k dist/app.min.js.map
|
||||
@gzip -k dist/app.min.js
|
||||
@find dist -type f -printf "%kK\\t%h/%f\\n" | sort -k 2
|
||||
@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 ../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.gz ../static/js/app.min.js.map.gz
|
||||
|
||||
docs:
|
||||
@rm -Rf generated-docs
|
||||
@purs docs ".spago/*/*/src/**/*.purs" --format html
|
||||
@npm run purs -- docs ".spago/*/*/src/**/*.purs" --format html
|
||||
|
||||
docset: docs
|
||||
@(cd docset; python3 ./gen-docset.py)
|
||||
clean:
|
||||
rm -f dist/*
|
||||
|
||||
# inotifywait -m -r -q -e close_write --format '%T %w%f' --timefmt '%T' src | while read FILE; do echo $FILE; make; done
|
||||
# inotifywait -m -r -q -e close_write --format '%T %w%f' --timefmt '%T' src | while read FILE; do echo $FILE; make build; done
|
||||
|
|
|
@ -12,7 +12,7 @@ from html import unescape
|
|||
from bs4 import BeautifulSoup
|
||||
|
||||
class Generator:
|
||||
GENERATED_DOCS = '../generated-docs'
|
||||
GENERATED_DOCS = '../generated-docs/html'
|
||||
OUTPUT = 'purescript-local.docset'
|
||||
|
||||
def documents_path(self, *paths):
|
||||
|
|
10690
purs/package-lock.json
generated
10690
purs/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -2,17 +2,21 @@
|
|||
"name": "espial",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"spago": "spago",
|
||||
"purs": "purs",
|
||||
"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",
|
||||
"parcel-build": "parcel build dist/bundle.js --out-file dist/app.min.js --public-url /static/js/"
|
||||
"esbuild-bundle": "esbuild ./output/Main/index.js --bundle --format=iife --global-name=PS --target=chrome58,firefox57,edge18 --outfile=dist/app.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": {
|
||||
"purescript": "^0.13.3",
|
||||
"spago": "^0.10.0",
|
||||
"marked": "^0.7.0",
|
||||
"moment": "^2.24.0",
|
||||
"parcel-bundler": "^1.12.3",
|
||||
"terser": "^4.0.0"
|
||||
"esbuild": "^0.15.12",
|
||||
"purescript": "^0.15.6",
|
||||
"spago": "0.20.9"
|
||||
},
|
||||
"dependencies": {}
|
||||
"dependencies": {
|
||||
"dompurify": "^2.4.0",
|
||||
"marked": "^4.1.1",
|
||||
"moment": "^2.29.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,42 @@
|
|||
let mkPackage =
|
||||
https://raw.githubusercontent.com/purescript/package-sets/psc-0.13.3-20190818/src/mkPackage.dhall sha256:0b197efa1d397ace6eb46b243ff2d73a3da5638d8d0ac8473e8e4a8fc528cf57
|
||||
{-
|
||||
### Overriding/Patching a package
|
||||
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 =
|
||||
https://raw.githubusercontent.com/purescript/package-sets/psc-0.13.3-20190818/src/packages.dhall sha256:c95c4a8b8033a48a350106b759179f68a695c7ea2208228c522866fd43814dc8
|
||||
https://raw.githubusercontent.com/purescript/package-sets/psc-0.15.4-20221026/src/packages.dhall
|
||||
sha256:8dc0b394f5861bb0136f652f3f826a88eaffb2bc0ecf0251468ed668102f5d0c
|
||||
|
||||
let overrides = {=}
|
||||
|
||||
let additions = {=}
|
||||
|
||||
in upstream // overrides // additions
|
||||
in upstream
|
||||
with simple-json =
|
||||
{ dependencies =
|
||||
[ "arrays"
|
||||
, "exceptions"
|
||||
, "foreign"
|
||||
, "foreign-object"
|
||||
, "nullable"
|
||||
, "prelude"
|
||||
, "record"
|
||||
, "typelevel-prelude"
|
||||
, "variant"
|
||||
]
|
||||
, repo = "https://github.com/justinwoo/purescript-simple-json.git"
|
||||
, version = "v9.0.0"
|
||||
}
|
||||
|
|
|
@ -1,26 +1,41 @@
|
|||
{ sources =
|
||||
[ "src/**/*.purs", "test/**/*.purs" ]
|
||||
, name =
|
||||
"espial"
|
||||
{ sources = [ "src/**/*.purs", "test/**/*.purs" ]
|
||||
, name = "espial"
|
||||
, dependencies =
|
||||
[ "aff"
|
||||
, "simple-json"
|
||||
, "affjax"
|
||||
, "argonaut"
|
||||
, "arrays"
|
||||
, "console"
|
||||
, "debug"
|
||||
, "effect"
|
||||
, "either"
|
||||
, "functions"
|
||||
, "halogen"
|
||||
, "prelude"
|
||||
, "psci-support"
|
||||
, "strings"
|
||||
, "transformers"
|
||||
, "web-html"
|
||||
, "profunctor-lenses"
|
||||
]
|
||||
, packages =
|
||||
./packages.dhall
|
||||
[ "aff"
|
||||
, "affjax"
|
||||
, "affjax-web"
|
||||
, "argonaut"
|
||||
, "arrays"
|
||||
, "console"
|
||||
, "const"
|
||||
, "dom-indexed"
|
||||
, "effect"
|
||||
, "either"
|
||||
, "foldable-traversable"
|
||||
, "foreign"
|
||||
, "foreign-object"
|
||||
, "form-urlencoded"
|
||||
, "functions"
|
||||
, "halogen"
|
||||
, "http-methods"
|
||||
, "integers"
|
||||
, "js-uri"
|
||||
, "maybe"
|
||||
, "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
|
||||
}
|
||||
|
|
|
@ -2,15 +2,16 @@ module App where
|
|||
|
||||
import Prelude
|
||||
|
||||
import Affjax (Response, ResponseFormatError)
|
||||
import Affjax (defaultRequest) as AX
|
||||
import Affjax as Ax
|
||||
import Affjax.Web (Response, Error)
|
||||
import Affjax.Web (defaultRequest) as AX
|
||||
import Affjax.Web as Ax
|
||||
import Affjax.RequestBody as AXReq
|
||||
import Affjax.RequestHeader (RequestHeader(..))
|
||||
import Affjax.ResponseFormat as AXRes
|
||||
import Data.Argonaut (Json)
|
||||
import Affjax.StatusCode (StatusCode(..))
|
||||
import Data.Argonaut (decodeJson)
|
||||
import Data.Array ((:))
|
||||
import Data.Either (Either(..))
|
||||
import Data.Either (Either(..), hush)
|
||||
import Data.FormURLEncoded (FormURLEncoded)
|
||||
import Data.HTTP.Method (Method(..))
|
||||
import Data.Maybe (Maybe(..))
|
||||
|
@ -18,7 +19,7 @@ import Data.MediaType.Common (applicationFormURLEncoded, applicationJSON)
|
|||
import Effect.Aff (Aff)
|
||||
import Effect.Class (liftEffect)
|
||||
import Globals (app')
|
||||
import Model (Bookmark, Bookmark'(..), Note, Note'(..), AccountSettings, AccountSettings'(..))
|
||||
import Model (Bookmark, Bookmark'(..), Note, Note'(..), AccountSettings, AccountSettings'(..), TagCloudMode, TagCloudMode'(..), TagCloud)
|
||||
import Simple.JSON as J
|
||||
import Web.HTML (window)
|
||||
import Web.HTML.Location (reload)
|
||||
|
@ -34,28 +35,46 @@ toggleStar bid action = do
|
|||
let path = "bm/" <> show bid <> "/" <> show action
|
||||
void (fetchUrlEnc POST path Nothing AXRes.ignore)
|
||||
|
||||
destroy :: Int -> Aff (Response (Either ResponseFormatError Unit))
|
||||
destroy :: Int -> Aff (Either Error (Response Unit))
|
||||
destroy bid =
|
||||
fetchUrlEnc DELETE ("bm/" <> show bid) Nothing AXRes.ignore
|
||||
|
||||
markRead :: Int -> Aff (Response (Either ResponseFormatError Unit))
|
||||
markRead :: Int -> Aff (Either Error (Response Unit))
|
||||
markRead bid = do
|
||||
let path = "bm/" <> show bid <> "/read"
|
||||
fetchUrlEnc POST path Nothing AXRes.ignore
|
||||
|
||||
editBookmark :: Bookmark -> Aff (Response (Either ResponseFormatError Unit))
|
||||
editBookmark :: Bookmark -> Aff (Either Error (Response String))
|
||||
editBookmark bm = do
|
||||
fetchJson POST "api/add" (Just (Bookmark' bm)) AXRes.ignore
|
||||
fetchJson POST "api/add" (Just (Bookmark' bm)) AXRes.string
|
||||
|
||||
editNote :: Note -> Aff (Response (Either ResponseFormatError Json))
|
||||
editNote :: Note -> Aff (Either Error (Response String))
|
||||
editNote bm = do
|
||||
fetchJson POST "api/note/add" (Just (Note' bm)) AXRes.json
|
||||
fetchJson POST "api/note/add" (Just (Note' bm)) AXRes.string
|
||||
|
||||
destroyNote :: Int -> Aff (Response (Either ResponseFormatError Unit))
|
||||
lookupTitle :: Bookmark -> Aff (Maybe String)
|
||||
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
|
||||
fetchUrlEnc DELETE ("api/note/" <> show nid) Nothing AXRes.ignore
|
||||
|
||||
editAccountSettings :: AccountSettings -> Aff (Response (Either ResponseFormatError Unit))
|
||||
editAccountSettings :: AccountSettings -> Aff (Either Error (Response Unit))
|
||||
editAccountSettings us = do
|
||||
fetchJson POST "api/accountSettings" (Just (AccountSettings' us)) AXRes.ignore
|
||||
|
||||
|
@ -73,7 +92,7 @@ fetchJson
|
|||
-> String
|
||||
-> Maybe b
|
||||
-> AXRes.ResponseFormat a
|
||||
-> Aff (Response (Either ResponseFormatError a))
|
||||
-> Aff (Either Error (Response a))
|
||||
fetchJson method path content rt =
|
||||
fetchPath method path [ContentType applicationJSON] (AXReq.string <<< J.writeJSON <$> content) rt
|
||||
|
||||
|
@ -83,7 +102,7 @@ fetchUrlEnc
|
|||
-> String
|
||||
-> Maybe FormURLEncoded
|
||||
-> AXRes.ResponseFormat a
|
||||
-> Aff (Response (Either ResponseFormatError a))
|
||||
-> Aff (Either Error (Response a))
|
||||
fetchUrlEnc method path content rt =
|
||||
fetchPath method path [ContentType applicationFormURLEncoded] (AXReq.FormURLEncoded <$> content) rt
|
||||
|
||||
|
@ -94,7 +113,7 @@ fetchPath
|
|||
-> Array RequestHeader
|
||||
-> Maybe AXReq.RequestBody
|
||||
-> AXRes.ResponseFormat a
|
||||
-> Aff (Response (Either ResponseFormatError a))
|
||||
-> Aff (Either Error (Response a))
|
||||
fetchPath method path headers content rt =
|
||||
fetchUrl method ((app' unit).homeR <> path) headers content rt
|
||||
|
||||
|
@ -105,7 +124,7 @@ fetchUrl
|
|||
-> Array RequestHeader
|
||||
-> Maybe AXReq.RequestBody
|
||||
-> AXRes.ResponseFormat a
|
||||
-> Aff (Response (Either ResponseFormatError a))
|
||||
-> Aff (Either Error (Response a))
|
||||
fetchUrl method url headers content rt =
|
||||
Ax.request
|
||||
AX.defaultRequest
|
||||
|
|
|
@ -4,14 +4,12 @@ import Prelude hiding (div)
|
|||
|
||||
import App (editAccountSettings)
|
||||
import Data.Lens (Lens', lens, use, (%=))
|
||||
import Data.Maybe (Maybe(..))
|
||||
import Effect.Aff (Aff)
|
||||
import Globals (app')
|
||||
import Halogen as H
|
||||
import Halogen.HTML (HTML, div, input, text)
|
||||
import Halogen.HTML (div, input, text)
|
||||
import Halogen.HTML.Elements (label)
|
||||
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 Util (class_)
|
||||
import Web.Event.Event (Event)
|
||||
|
@ -34,7 +32,7 @@ data EditField
|
|||
|
||||
|
||||
-- | The bookmark component definition.
|
||||
usetting :: forall q i o. AccountSettings -> H.Component HTML q i o Aff
|
||||
usetting :: forall q i o. AccountSettings -> H.Component q i o Aff
|
||||
usetting u' =
|
||||
H.mkComponent
|
||||
{ initialState: const (mkState u')
|
||||
|
@ -42,7 +40,6 @@ usetting u' =
|
|||
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
|
||||
}
|
||||
where
|
||||
app = app' unit
|
||||
|
||||
mkState u =
|
||||
{ us: u
|
||||
|
@ -53,27 +50,27 @@ usetting u' =
|
|||
div [ class_ "settings-form" ]
|
||||
[ div [ class_ "fw7 mb2"] [ text "Account Settings" ]
|
||||
, div [ class_ "flex items-center mb2" ]
|
||||
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "archiveDefault", name "archiveDefault"
|
||||
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id "archiveDefault", name "archiveDefault"
|
||||
, checked (us.archiveDefault) , onChecked (editField EarchiveDefault) ]
|
||||
, label [ for "archiveDefault", class_ "lh-copy" ]
|
||||
[ text "Archive Non-Private Bookmarks (archive.li)" ]
|
||||
]
|
||||
, div [ class_ "flex items-center mb2" ]
|
||||
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "privateDefault", name "privateDefault"
|
||||
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id "privateDefault", name "privateDefault"
|
||||
, checked (us.privateDefault) , onChecked (editField EprivateDefault) ]
|
||||
, label [ for "privateDefault", class_ "lh-copy" ]
|
||||
[ text "Default new bookmarks to Private" ]
|
||||
]
|
||||
, div [ class_ "flex items-center mb2" ]
|
||||
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id_ "privacyLock", name "privacyLock"
|
||||
[ input [ type_ InputCheckbox , class_ "pointer mr2" , id "privacyLock", name "privacyLock"
|
||||
, checked (us.privacyLock) , onChecked (editField EprivacyLock) ]
|
||||
, label [ for "privacyLock", class_ "lh-copy" ]
|
||||
[ text "Privacy Lock (Private Account)" ]
|
||||
]
|
||||
]
|
||||
where
|
||||
editField :: forall a. (a -> EditField) -> a -> Maybe UAction
|
||||
editField f = Just <<< UEditField <<< f
|
||||
editField :: forall a. (a -> EditField) -> a -> UAction
|
||||
editField f = UEditField <<< f
|
||||
|
||||
handleAction :: UAction -> H.HalogenM UState UAction () o Aff Unit
|
||||
handleAction (UEditField f) = do
|
||||
|
@ -84,6 +81,6 @@ usetting u' =
|
|||
us <- use _us
|
||||
void $ H.liftAff (editAccountSettings us)
|
||||
|
||||
handleAction (USubmit e) = do
|
||||
handleAction (USubmit _) = do
|
||||
us <- use _us
|
||||
void $ H.liftAff (editAccountSettings us)
|
||||
|
|
|
@ -2,32 +2,36 @@ module Component.Add where
|
|||
|
||||
import Prelude hiding (div)
|
||||
|
||||
import App (destroy, editBookmark)
|
||||
import Data.Array (drop, foldMap)
|
||||
import Affjax (printError)
|
||||
import Affjax.StatusCode (StatusCode(..))
|
||||
import App (destroy, editBookmark, lookupTitle)
|
||||
import Data.Either (Either(..))
|
||||
import Data.Lens (Lens', lens, use, (%=), (.=))
|
||||
import Data.Maybe (Maybe(..), maybe)
|
||||
import Data.Maybe (Maybe(..), fromMaybe, isJust, maybe)
|
||||
import Data.Monoid (guard)
|
||||
import Data.String (null)
|
||||
import Data.String (split) as S
|
||||
import Data.String.Pattern (Pattern(..))
|
||||
import Data.String (Pattern(..), null, stripPrefix)
|
||||
import Data.Tuple (fst, snd)
|
||||
import Effect.Aff (Aff)
|
||||
import Effect.Class (liftEffect)
|
||||
import Globals (app', closeWindow, mmoment8601)
|
||||
import Effect.Console (log)
|
||||
import Globals (closeWindow, mmoment8601)
|
||||
import Halogen as H
|
||||
import Halogen.HTML (HTML, br_, button, div, form, input, label, p, span, table, tbody_, td, td_, text, textarea, tr_)
|
||||
import Halogen.HTML (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.Properties (autofocus, ButtonType(..), InputType(..), autocomplete, checked, for, id_, name, required, rows, title, type_, value)
|
||||
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, autofocus, checked, disabled, for, id, name, required, rows, title, type_, value)
|
||||
import Model (Bookmark)
|
||||
import Util (_curQuerystring, _loc, _lookupQueryStringValue, attr, class_, ifElseH, whenH)
|
||||
import Util (_curQuerystring, _loc, _doc, _lookupQueryStringValue, attr, class_, ifElseH, whenH)
|
||||
import Web.Event.Event (Event, preventDefault)
|
||||
import Web.HTML (window)
|
||||
import Web.HTML.Location (setHref)
|
||||
import Web.HTML.HTMLDocument (referrer)
|
||||
import Web.HTML.Location (setHref, origin)
|
||||
import DOM.HTML.Indexed.AutocompleteType (AutocompleteType(..))
|
||||
|
||||
data BAction
|
||||
= BEditField EditField
|
||||
| BEditSubmit Event
|
||||
| BDeleteAsk Boolean
|
||||
| BLookupTitle
|
||||
| BDestroy
|
||||
|
||||
data EditField
|
||||
|
@ -42,7 +46,9 @@ type BState =
|
|||
{ bm :: Bookmark
|
||||
, edit_bm :: Bookmark
|
||||
, deleteAsk :: Boolean
|
||||
, loading :: Boolean
|
||||
, destroyed :: Boolean
|
||||
, apiError :: Maybe String
|
||||
}
|
||||
|
||||
_bm :: Lens' BState Bookmark
|
||||
|
@ -51,7 +57,10 @@ _bm = lens _.bm (_ { bm = _ })
|
|||
_edit_bm :: Lens' BState Bookmark
|
||||
_edit_bm = lens _.edit_bm (_ { edit_bm = _ })
|
||||
|
||||
addbmark :: forall q i o. Bookmark -> H.Component HTML q i o Aff
|
||||
_apiError :: Lens' BState (Maybe String)
|
||||
_apiError = lens _.apiError (_ { apiError = _ })
|
||||
|
||||
addbmark :: forall q i o. Bookmark -> H.Component q i o Aff
|
||||
addbmark b' =
|
||||
H.mkComponent
|
||||
{ initialState: const (mkState b')
|
||||
|
@ -59,59 +68,64 @@ addbmark b' =
|
|||
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
|
||||
}
|
||||
where
|
||||
app = app' unit
|
||||
|
||||
mkState b =
|
||||
{ bm: b
|
||||
, edit_bm: b
|
||||
, deleteAsk: false
|
||||
, destroyed: false
|
||||
, loading: false
|
||||
, apiError: Nothing
|
||||
}
|
||||
|
||||
render :: forall m. BState -> H.ComponentHTML BAction () m
|
||||
render s@{ bm, edit_bm } =
|
||||
render s@{ bm, edit_bm, apiError } =
|
||||
ifElseH (not s.destroyed)
|
||||
display_edit
|
||||
display_destroyed
|
||||
where
|
||||
display_edit _ =
|
||||
form [ onSubmit (Just <<< BEditSubmit) ]
|
||||
form [ onSubmit BEditSubmit ]
|
||||
[ table [ class_ "w-100" ]
|
||||
[ tbody_
|
||||
[ tr_
|
||||
[ td [ class_ "w1" ] [ ]
|
||||
, td_ [ whenH (bm.bid > 0)
|
||||
display_exists
|
||||
display_exists,
|
||||
whenH (isJust apiError)
|
||||
(alert_notification (fromMaybe "" apiError))
|
||||
]
|
||||
]
|
||||
, tr_
|
||||
[ td_ [ label [ for "url" ] [ text "URL" ] ]
|
||||
, td_ [ input [ type_ InputUrl , id_ "url", class_ "w-100 mv1" , required true, name "url", autofocus (null bm.url)
|
||||
, value (edit_bm.url) , onValueChange (editField Eurl)] ]
|
||||
, td_ [ input [ type_ InputUrl , id "url", class_ "w-100 mv1" , required true, name "url", autofocus (null bm.url)
|
||||
, value (edit_bm.url) , onValueChange (editField Eurl)] ]
|
||||
]
|
||||
, tr_
|
||||
[ td_ [ label [ for "title" ] [ text "title" ] ]
|
||||
, td_ [ input [ type_ InputText , id_ "title", class_ "w-100 mv1" , name "title"
|
||||
, value (edit_bm.title) , onValueChange (editField Etitle)] ]
|
||||
, td [class_ "flex"]
|
||||
[ input [ type_ InputText , id "title", class_ "w-100 mv1 flex-auto" , name "title" , 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_
|
||||
[ 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)] ]
|
||||
]
|
||||
, tr_
|
||||
[ td_ [ label [ for "tags" ] [ text "tags" ] ]
|
||||
, td_ [ input [ type_ InputText , id_ "tags", class_ "w-100 mv1" , name "tags", autocomplete false, attr "autocapitalize" "off", autofocus (not $ null bm.url)
|
||||
, td_ [ input [ type_ InputText , id "tags", class_ "w-100 mv1" , name "tags", autocomplete AutocompleteOff, attr "autocapitalize" "off", autofocus (not $ null bm.url)
|
||||
, value (edit_bm.tags) , onValueChange (editField Etags)] ]
|
||||
]
|
||||
, tr_
|
||||
[ td_ [ label [ for "private" ] [ text "private" ] ]
|
||||
, td_ [ input [ type_ InputCheckbox , id_ "private", class_ "private pointer" , name "private"
|
||||
, td_ [ input [ type_ InputCheckbox , id "private", class_ "private pointer" , name "private"
|
||||
, checked (edit_bm.private) , onChecked (editField Eprivate)] ]
|
||||
]
|
||||
, tr_
|
||||
[ td_ [ label [ for "toread" ] [ text "read later" ] ]
|
||||
, td_ [ input [ type_ InputCheckbox , id_ "toread", class_ "toread pointer" , name "toread"
|
||||
, td_ [ input [ type_ InputCheckbox , id "toread", class_ "toread pointer" , name "toread"
|
||||
, checked (edit_bm.toread) , onChecked (editField Etoread)] ]
|
||||
]
|
||||
, tr_
|
||||
|
@ -130,32 +144,46 @@ addbmark b' =
|
|||
[ text (maybe " " fst mmoment) ]
|
||||
, div [ class_ "edit_links dib ml1" ]
|
||||
[ div [ class_ "delete_link di" ]
|
||||
[ button ([ type_ ButtonButton, onClick \_ -> Just (BDeleteAsk true), class_ "delete" ] <> guard s.deleteAsk [ attr "hidden" "hidden" ]) [ text "delete" ]
|
||||
[ button ([ type_ ButtonButton, onClick \_ -> BDeleteAsk true, class_ "delete" ] <> guard s.deleteAsk [ attr "hidden" "hidden" ]) [ text "delete" ]
|
||||
, span ([ class_ "confirm red" ] <> guard (not s.deleteAsk) [ attr "hidden" "hidden" ])
|
||||
[ button [ type_ ButtonButton, onClick \_ -> Just (BDeleteAsk false)] [ text "cancel / " ]
|
||||
, button [ type_ ButtonButton, onClick \_ -> Just BDestroy, class_ "red" ] [ text "destroy" ]
|
||||
[ button [ type_ ButtonButton, onClick \_ -> BDeleteAsk false] [ text "cancel / " ]
|
||||
, button [ type_ ButtonButton, onClick \_ -> 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"]
|
||||
|
||||
editField :: forall a. (a -> EditField) -> a -> Maybe BAction
|
||||
editField f = Just <<< BEditField <<< f
|
||||
editField :: forall a. (a -> EditField) -> a -> BAction
|
||||
editField f = BEditField <<< f
|
||||
mmoment = mmoment8601 bm.time
|
||||
toTextarea =
|
||||
drop 1
|
||||
<<< foldMap (\x -> [br_, text x])
|
||||
<<< S.split (Pattern "\n")
|
||||
-- toTextarea =
|
||||
-- drop 1
|
||||
-- <<< foldMap (\x -> [br_, text x])
|
||||
-- <<< S.split (Pattern "\n")
|
||||
|
||||
handleAction :: BAction -> H.HalogenM BState BAction () o Aff Unit
|
||||
handleAction (BDeleteAsk e) = do
|
||||
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
|
||||
bid <- H.gets _.bm.bid
|
||||
void $ H.liftAff (destroy bid)
|
||||
H.modify_ (_ { destroyed = true })
|
||||
|
||||
handleAction (BEditField f) = do
|
||||
_edit_bm %= case f of
|
||||
Eurl e -> _ { url = e }
|
||||
|
@ -164,14 +192,29 @@ addbmark b' =
|
|||
Etags e -> _ { tags = e }
|
||||
Eprivate e -> _ { private = e }
|
||||
Etoread e -> _ { toread = e }
|
||||
|
||||
handleAction (BEditSubmit e) = do
|
||||
H.liftEffect (preventDefault e)
|
||||
liftEffect (preventDefault e)
|
||||
edit_bm <- use _edit_bm
|
||||
void $ H.liftAff (editBookmark edit_bm)
|
||||
_bm .= edit_bm
|
||||
loc <- liftEffect _loc
|
||||
win <- liftEffect window
|
||||
qs <- liftEffect _curQuerystring
|
||||
case _lookupQueryStringValue qs "next" of
|
||||
Just n -> liftEffect (setHref n loc)
|
||||
_ -> liftEffect (closeWindow win)
|
||||
_apiError .= Nothing
|
||||
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
|
||||
qs <- liftEffect $ _curQuerystring
|
||||
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)
|
||||
|
|
|
@ -1,39 +1,38 @@
|
|||
module Component.BList where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Component.BMark (BMessage(..), BSlot, bmark)
|
||||
import Model (Bookmark, BookmarkId)
|
||||
|
||||
import Data.Array (filter)
|
||||
import Data.Maybe (Maybe(..))
|
||||
import Effect.Aff (Aff)
|
||||
import Halogen as H
|
||||
import Halogen.HTML as HH
|
||||
import Data.Symbol (SProxy(..))
|
||||
|
||||
data LAction =
|
||||
HandleBMessage BookmarkId BMessage
|
||||
|
||||
type ChildSlots =
|
||||
( bookmark :: BSlot Int
|
||||
)
|
||||
|
||||
_bookmark = SProxy :: SProxy "bookmark"
|
||||
|
||||
blist :: forall q i o. Array Bookmark -> H.Component HH.HTML q i o Aff
|
||||
blist st =
|
||||
H.mkComponent
|
||||
{ initialState: const st
|
||||
, render
|
||||
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
|
||||
}
|
||||
where
|
||||
|
||||
render :: Array Bookmark -> H.ComponentHTML LAction ChildSlots Aff
|
||||
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
|
||||
H.modify_ (filter (\b -> b.bid /= bid))
|
||||
module Component.BList where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Component.BMark (BMessage(..), BSlot, bmark)
|
||||
import Model (Bookmark, BookmarkId)
|
||||
|
||||
import Data.Array (filter)
|
||||
import Effect.Aff (Aff)
|
||||
import Halogen as H
|
||||
import Halogen.HTML as HH
|
||||
import Type.Proxy (Proxy(..))
|
||||
|
||||
data LAction =
|
||||
HandleBMessage BookmarkId BMessage
|
||||
|
||||
type ChildSlots =
|
||||
( bookmark :: BSlot Int
|
||||
)
|
||||
|
||||
_bookmark = Proxy :: Proxy "bookmark"
|
||||
|
||||
blist :: forall q i o. Array Bookmark -> H.Component q i o Aff
|
||||
blist st =
|
||||
H.mkComponent
|
||||
{ initialState: const st
|
||||
, render
|
||||
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
|
||||
}
|
||||
where
|
||||
|
||||
render :: Array Bookmark -> H.ComponentHTML LAction ChildSlots Aff
|
||||
render bms =
|
||||
HH.div_ $ map (\b -> HH.slot _bookmark b.bid (bmark b) unit (HandleBMessage b.bid)) bms
|
||||
|
||||
handleAction :: LAction -> H.HalogenM (Array Bookmark) LAction ChildSlots o Aff Unit
|
||||
handleAction (HandleBMessage bid BNotifyRemove) = do
|
||||
H.modify_ (filter (\b -> b.bid /= bid))
|
||||
|
|
|
@ -1,244 +1,286 @@
|
|||
module Component.BMark where
|
||||
|
||||
import Prelude hiding (div)
|
||||
|
||||
import App (StarAction(..), destroy, editBookmark, markRead, toggleStar)
|
||||
import Component.Markdown as Markdown
|
||||
import Data.Const (Const)
|
||||
import Data.Lens (Lens', lens, use, (%=), (.=))
|
||||
import Data.Maybe (Maybe(..), fromMaybe, isJust)
|
||||
import Data.Monoid (guard)
|
||||
import Data.Nullable (toMaybe)
|
||||
import Data.String (null, split, take) as S
|
||||
import Data.String.Pattern (Pattern(..))
|
||||
import Data.Symbol (SProxy(..))
|
||||
import Effect.Aff (Aff)
|
||||
import Globals (app')
|
||||
import Halogen as H
|
||||
import Halogen.HTML (HTML, a, br_, button, div, div_, form, input, label, span, text, textarea)
|
||||
import Halogen.HTML as HH
|
||||
import Halogen.HTML.Events (onSubmit, onValueChange, onChecked, onClick)
|
||||
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, checked, for, href, id_, name, required, rows, target, title, type_, value)
|
||||
import Model (Bookmark)
|
||||
import Util (attr, class_, fromNullableStr, ifElseH, whenH, whenA)
|
||||
import Web.Event.Event (Event, preventDefault)
|
||||
|
||||
-- | UI Events
|
||||
data BAction
|
||||
= BStar Boolean
|
||||
| BDeleteAsk Boolean
|
||||
| BDestroy
|
||||
| BEdit Boolean
|
||||
| BEditField EditField
|
||||
| BEditSubmit Event
|
||||
| BMarkRead
|
||||
|
||||
-- | FormField Edits
|
||||
data EditField
|
||||
= Eurl String
|
||||
| Etitle String
|
||||
| Edescription String
|
||||
| Etags String
|
||||
| Eprivate Boolean
|
||||
| Etoread Boolean
|
||||
|
||||
-- | Messages to parent
|
||||
data BMessage
|
||||
= BNotifyRemove
|
||||
|
||||
type BSlot = H.Slot (Const Void) BMessage
|
||||
|
||||
type BState =
|
||||
{ bm :: Bookmark
|
||||
, edit_bm :: Bookmark
|
||||
, deleteAsk:: Boolean
|
||||
, edit :: Boolean
|
||||
}
|
||||
|
||||
_bm :: Lens' BState Bookmark
|
||||
_bm = lens _.bm (_ { bm = _ })
|
||||
|
||||
_edit_bm :: Lens' BState Bookmark
|
||||
_edit_bm = lens _.edit_bm (_ { edit_bm = _ })
|
||||
|
||||
_edit :: Lens' BState Boolean
|
||||
_edit = lens _.edit (_ { edit = _ })
|
||||
|
||||
_markdown = SProxy :: SProxy "markdown"
|
||||
|
||||
type ChildSlots =
|
||||
( markdown :: Markdown.Slot Unit
|
||||
)
|
||||
|
||||
bmark :: forall q i. Bookmark -> H.Component HTML q i BMessage Aff
|
||||
bmark b' =
|
||||
H.mkComponent
|
||||
{ initialState: const (mkState b')
|
||||
, render
|
||||
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
|
||||
}
|
||||
where
|
||||
app = app' unit
|
||||
|
||||
mkState b =
|
||||
{ bm: b
|
||||
, edit_bm: b
|
||||
, deleteAsk: false
|
||||
, edit: false
|
||||
}
|
||||
|
||||
render :: BState -> H.ComponentHTML BAction ChildSlots Aff
|
||||
render s@{ bm, edit_bm } =
|
||||
div [ id_ (show bm.bid) , class_ ("bookmark w-100 mw7 pa1 mb3" <> guard bm.private " private")] $
|
||||
[ whenH app.dat.isowner
|
||||
star
|
||||
, ifElseH s.edit
|
||||
display_edit
|
||||
display
|
||||
]
|
||||
|
||||
where
|
||||
|
||||
star _ =
|
||||
div [ class_ ("star fl pointer" <> guard bm.selected " selected") ]
|
||||
[ button [ class_ "moon-gray", onClick \_ -> Just (BStar (not bm.selected)) ] [ text "✭" ] ]
|
||||
|
||||
display _ =
|
||||
div [ class_ "display" ] $
|
||||
[ a [ href bm.url, target "_blank", class_ ("link f5 lh-title" <> guard bm.toread " unread")]
|
||||
[ text $ if S.null bm.title then "[no title]" else bm.title ]
|
||||
, br_
|
||||
, a [ href bm.url , class_ "link f7 gray hover-blue" ] [ text bm.url ]
|
||||
, a [ href (fromMaybe ("http://archive.is/" <> bm.url) (toMaybe bm.archiveUrl))
|
||||
, class_ ("link f7 gray hover-blue ml2" <> (guard (isJust (toMaybe bm.archiveUrl)) " green"))
|
||||
, target "_blank", title "archive link"]
|
||||
[ if isJust (toMaybe bm.archiveUrl) then text "☑" else text "☐" ]
|
||||
, br_
|
||||
, div [ class_ "description mt1 mid-gray" ] [ HH.slot _markdown unit Markdown.component bm.description absurd ]
|
||||
, div [ class_ "tags" ] $
|
||||
whenA (not (S.null bm.tags)) $ \_ ->
|
||||
map (\tag -> a [ class_ ("link tag mr1" <> guard (S.take 1 tag == ".") " private")
|
||||
, href (linkToFilterTag tag) ]
|
||||
[ text tag ])
|
||||
(S.split (Pattern " ") bm.tags)
|
||||
|
||||
, a [ class_ "link f7 dib gray w4", href (linkToFilterSingle bm.slug) ]
|
||||
[ text shtime ]
|
||||
|
||||
-- links
|
||||
, whenH app.dat.isowner $ \_ ->
|
||||
div [ class_ "edit_links di" ]
|
||||
[ button [ type_ ButtonButton, onClick \_ -> Just (BEdit true), class_ "edit light-silver hover-blue" ] [ text "edit " ]
|
||||
, div [ class_ "delete_link di" ]
|
||||
[ button [ type_ ButtonButton, onClick \_ -> Just (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 \_ -> Just (BDeleteAsk false)] [ text "cancel / " ]
|
||||
, button [ type_ ButtonButton, onClick \_ -> Just BDestroy, class_ "red" ] [ text "destroy" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
, whenH app.dat.isowner $ \_ ->
|
||||
div [ class_ "read di" ] $
|
||||
guard bm.toread
|
||||
[ text " "
|
||||
, button [ onClick \_ -> Just BMarkRead, class_ "mark_read" ] [ text "mark as read"]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
display_edit _ =
|
||||
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"
|
||||
, value (edit_bm.url) , onValueChange (editField Eurl) ]
|
||||
, br_
|
||||
, div_ [ text "title" ]
|
||||
, input [ type_ InputText , class_ "title w-100 mb2 pt1 f7 edit_form_input" , name "title"
|
||||
, value (edit_bm.title) , onValueChange (editField Etitle) ]
|
||||
, br_
|
||||
, div_ [ text "description" ]
|
||||
, textarea [ class_ "description w-100 mb1 pt1 f7 edit_form_input" , name "description", rows 5
|
||||
, value (edit_bm.description) , onValueChange (editField Edescription) ]
|
||||
, br_
|
||||
, div [ id_ "tags_input_box"]
|
||||
[ div_ [ text "tags" ]
|
||||
, input [ type_ InputText , class_ "tags w-100 mb1 pt1 f7 edit_form_input" , name "tags"
|
||||
, autocomplete false, attr "autocapitalize" "off"
|
||||
, value (edit_bm.tags) , onValueChange (editField Etags) ]
|
||||
, br_
|
||||
]
|
||||
, div [ class_ "edit_form_checkboxes mv3"]
|
||||
[ input [ type_ InputCheckbox , class_ "private pointer" , id_ "edit_private", name "private"
|
||||
, checked (edit_bm.private) , onChecked (editField Eprivate) ]
|
||||
, text " "
|
||||
, label [ for "edit_private" , class_ "mr2" ] [ text "private" ]
|
||||
, text " "
|
||||
, input [ type_ InputCheckbox , class_ "toread pointer" , id_ "edit_toread", name "toread"
|
||||
, checked (edit_bm.toread) , onChecked (editField Etoread) ]
|
||||
, text " "
|
||||
, label [ for "edit_toread" ] [ text "to-read" ]
|
||||
, br_
|
||||
]
|
||||
, input [ type_ InputSubmit , class_ "mr1 pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "save" ]
|
||||
, text " "
|
||||
, input [ type_ InputReset , class_ "pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "cancel"
|
||||
, onClick \_ -> Just (BEdit false) ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
editField :: forall a. (a -> EditField) -> a -> Maybe BAction
|
||||
editField f = Just <<< BEditField <<< f
|
||||
linkToFilterSingle slug = fromNullableStr app.userR <> "/b:" <> slug
|
||||
linkToFilterTag tag = fromNullableStr app.userR <> "/t:" <> tag
|
||||
shtime = S.take 16 bm.time `append` "Z"
|
||||
|
||||
handleAction :: BAction -> H.HalogenM BState BAction ChildSlots BMessage Aff Unit
|
||||
|
||||
-- | Star
|
||||
handleAction (BStar e) = do
|
||||
bm <- use _bm
|
||||
H.liftAff (toggleStar bm.bid (if e then Star else UnStar))
|
||||
_bm %= _ { selected = e }
|
||||
_edit_bm %= _ { selected = e }
|
||||
|
||||
-- | Delete
|
||||
handleAction (BDeleteAsk e) = do
|
||||
H.modify_ (_ { deleteAsk = e })
|
||||
|
||||
-- | Destroy
|
||||
handleAction (BDestroy) = do
|
||||
bm <- use _bm
|
||||
void $ H.liftAff (destroy bm.bid)
|
||||
H.raise BNotifyRemove
|
||||
|
||||
-- | Mark Read
|
||||
handleAction (BMarkRead) = do
|
||||
bm <- use _bm
|
||||
void (H.liftAff (markRead bm.bid))
|
||||
_bm %= _ { toread = false }
|
||||
|
||||
-- | Start/Stop Editing
|
||||
handleAction (BEdit e) = do
|
||||
bm <- use _bm
|
||||
_edit_bm .= bm
|
||||
_edit .= e
|
||||
|
||||
-- | 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 }
|
||||
|
||||
-- | Submit
|
||||
handleAction (BEditSubmit e) = do
|
||||
H.liftEffect (preventDefault e)
|
||||
edit_bm <- use _edit_bm
|
||||
void $ H.liftAff (editBookmark edit_bm)
|
||||
_bm .= edit_bm
|
||||
_edit .= false
|
||||
module Component.BMark where
|
||||
|
||||
import Prelude hiding (div)
|
||||
|
||||
import Affjax (printError)
|
||||
import Affjax.StatusCode (StatusCode(..))
|
||||
import App (StarAction(..), destroy, editBookmark, markRead, toggleStar, lookupTitle)
|
||||
import Component.Markdown as Markdown
|
||||
import Data.Const (Const)
|
||||
import Data.Either (Either(..))
|
||||
import Data.Lens (Lens', lens, use, (%=), (.=))
|
||||
import Data.Maybe (Maybe(..), fromMaybe, isJust)
|
||||
import Data.Monoid (guard)
|
||||
import Data.Nullable (toMaybe)
|
||||
import Data.String (null, split, take, replaceAll) as S
|
||||
import Data.String.Pattern (Pattern(..), Replacement(..))
|
||||
import Effect.Aff (Aff)
|
||||
import Effect.Class (liftEffect)
|
||||
import Effect.Class.Console (log)
|
||||
import Globals (app', setFocus, toLocaleDateString)
|
||||
import Halogen as H
|
||||
import Halogen.HTML (a, br_, button, div, div_, form, input, label, span, text, textarea)
|
||||
import Halogen.HTML as HH
|
||||
import Halogen.HTML.Events (onSubmit, onValueChange, onChecked, onClick)
|
||||
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autocomplete, checked, disabled, for, href, id, name, required, rows, target, title, type_, value)
|
||||
import Model (Bookmark)
|
||||
import Type.Proxy (Proxy(..))
|
||||
import Util (attr, class_, encodeTag, fromNullableStr, ifElseH, whenA, whenH)
|
||||
import Web.Event.Event (Event, preventDefault)
|
||||
import DOM.HTML.Indexed.AutocompleteType (AutocompleteType(..))
|
||||
|
||||
-- | UI Events
|
||||
data BAction
|
||||
= BStar Boolean
|
||||
| BDeleteAsk Boolean
|
||||
| BLookupTitle
|
||||
| BDestroy
|
||||
| BEdit Boolean
|
||||
| BEditField EditField
|
||||
| BEditSubmit Event
|
||||
| BMarkRead
|
||||
|
||||
-- | FormField Edits
|
||||
data EditField
|
||||
= Eurl String
|
||||
| Etitle String
|
||||
| Edescription String
|
||||
| Etags String
|
||||
| Eprivate Boolean
|
||||
| Etoread Boolean
|
||||
|
||||
-- | Messages to parent
|
||||
data BMessage
|
||||
= BNotifyRemove
|
||||
|
||||
type BSlot = H.Slot (Const Void) BMessage
|
||||
|
||||
type BState =
|
||||
{ bm :: Bookmark
|
||||
, edit_bm :: Bookmark
|
||||
, deleteAsk:: Boolean
|
||||
, edit :: Boolean
|
||||
, loading :: Boolean
|
||||
, apiError :: Maybe String
|
||||
}
|
||||
|
||||
_bm :: Lens' BState Bookmark
|
||||
_bm = lens _.bm (_ { bm = _ })
|
||||
|
||||
_edit_bm :: Lens' BState Bookmark
|
||||
_edit_bm = lens _.edit_bm (_ { edit_bm = _ })
|
||||
|
||||
_edit :: Lens' BState Boolean
|
||||
_edit = lens _.edit (_ { edit = _ })
|
||||
|
||||
_apiError :: Lens' BState (Maybe String)
|
||||
_apiError = lens _.apiError (_ { apiError = _ })
|
||||
|
||||
_markdown = Proxy :: Proxy "markdown"
|
||||
|
||||
type ChildSlots =
|
||||
( markdown :: Markdown.Slot Unit
|
||||
)
|
||||
|
||||
bmark :: forall q i. Bookmark -> H.Component q i BMessage Aff
|
||||
bmark b' =
|
||||
H.mkComponent
|
||||
{ initialState: const (mkState b')
|
||||
, render
|
||||
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
|
||||
}
|
||||
where
|
||||
app = app' unit
|
||||
|
||||
mkState b =
|
||||
{ bm: b
|
||||
, edit_bm: b
|
||||
, deleteAsk: false
|
||||
, edit: false
|
||||
, loading: false
|
||||
, apiError: Nothing
|
||||
}
|
||||
|
||||
render :: BState -> H.ComponentHTML BAction ChildSlots Aff
|
||||
render s@{ bm, edit_bm, apiError } =
|
||||
div [ id (show bm.bid) , class_ ("bookmark w-100 mw7 pa1 mb3" <> guard bm.private " private")] $
|
||||
[ whenH app.dat.isowner
|
||||
star
|
||||
, ifElseH s.edit
|
||||
display_edit
|
||||
display
|
||||
]
|
||||
|
||||
where
|
||||
|
||||
star _ =
|
||||
div [ class_ ("star fl pointer" <> guard bm.selected " selected") ]
|
||||
[ button [ class_ "moon-gray", onClick \_ -> BStar (not bm.selected) ] [ text "✭" ] ]
|
||||
|
||||
display _ =
|
||||
div [ class_ "display" ] $
|
||||
[ a [ href bm.url, target "_blank", class_ ("link f5 lh-title" <> guard bm.toread " unread")]
|
||||
[ text $ if S.null bm.title then "[no title]" else bm.title ]
|
||||
, br_
|
||||
, a [ href bm.url , class_ "link f7 gray hover-blue" ] [ text bm.url ]
|
||||
, a [ href (fromMaybe ("http://archive.is/" <> bm.url) (toMaybe bm.archiveUrl))
|
||||
, class_ ("link f7 gray hover-blue ml2" <> (guard (isJust (toMaybe bm.archiveUrl)) " green"))
|
||||
, target "_blank", title "archive link"]
|
||||
[ if isJust (toMaybe bm.archiveUrl) then text "☑" else text "☐" ]
|
||||
, br_
|
||||
, div [ class_ "description mt1 mid-gray" ] [ HH.slot _markdown unit Markdown.component bm.description absurd ]
|
||||
, div [ class_ "tags" ] $
|
||||
whenA (not (S.null bm.tags)) $ \_ ->
|
||||
map (\tag -> a [ class_ ("link tag mr1" <> guard (S.take 1 tag == ".") " private")
|
||||
, href (linkToFilterTag tag) ]
|
||||
[ text tag ])
|
||||
(S.split (Pattern " ") bm.tags)
|
||||
|
||||
, a [ class_ "link f7 dib gray w4", href (linkToFilterSingle bm.slug), title shdatetime ]
|
||||
[ text shdate ]
|
||||
|
||||
-- links
|
||||
, whenH app.dat.isowner $ \_ ->
|
||||
div [ class_ "edit_links di" ]
|
||||
[ 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 / " ]
|
||||
, button [ type_ ButtonButton, onClick \_ -> BDestroy, class_ "red" ] [ text "destroy" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
, whenH app.dat.isowner $ \_ ->
|
||||
div [ class_ "read di" ] $
|
||||
guard bm.toread
|
||||
[ text " "
|
||||
, button [ onClick \_ -> BMarkRead, class_ "mark_read" ] [ text "mark as read"]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
display_edit _ =
|
||||
div [ class_ "edit_bookmark_form pa2 pt0 bg-white" ] $
|
||||
[ whenH (isJust apiError)
|
||||
(alert_notification (fromMaybe "" apiError))
|
||||
, form [ onSubmit BEditSubmit ]
|
||||
[ div_ [ text "url" ]
|
||||
, input [ type_ InputUrl , class_ "url w-100 mb2 pt1 edit_form_input" , required true , name "url"
|
||||
, value (edit_bm.url) , onValueChange (editField Eurl) ]
|
||||
, div_ [ text "title" ]
|
||||
, div [class_ "flex"]
|
||||
[input [ type_ InputText , class_ "title w-100 mb2 pt1 edit_form_input" , name "title"
|
||||
, value (edit_bm.title) , onValueChange (editField Etitle) ]
|
||||
, 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" ]
|
||||
]
|
||||
, div_ [ text "description" ]
|
||||
, textarea [ class_ "description w-100 mb1 pt1 edit_form_input" , name "description", rows 5
|
||||
, value (edit_bm.description) , onValueChange (editField Edescription) ]
|
||||
, div [ id "tags_input_box"]
|
||||
[ div_ [ text "tags" ]
|
||||
, input [ id (tagid edit_bm), type_ InputText , class_ "tags w-100 mb1 pt1 edit_form_input" , name "tags"
|
||||
, autocomplete AutocompleteOff, attr "autocapitalize" "off"
|
||||
, value (edit_bm.tags) , onValueChange (editField Etags) ]
|
||||
]
|
||||
, div [ class_ "edit_form_checkboxes mv3"]
|
||||
[ input [ type_ InputCheckbox , class_ "private pointer" , id "edit_private", name "private"
|
||||
, checked (edit_bm.private) , onChecked (editField Eprivate) ]
|
||||
, text " "
|
||||
, label [ for "edit_private" , class_ "mr2" ] [ text "private" ]
|
||||
, text " "
|
||||
, input [ type_ InputCheckbox , class_ "toread pointer" , id "edit_toread", name "toread"
|
||||
, checked (edit_bm.toread) , onChecked (editField Etoread) ]
|
||||
, text " "
|
||||
, label [ for "edit_toread" ] [ text "to-read" ]
|
||||
]
|
||||
, input [ type_ InputSubmit , class_ "mr1 pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "save" ]
|
||||
, text " "
|
||||
, input [ type_ InputReset , class_ "pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim" , value "cancel"
|
||||
, onClick \_ -> BEdit false ]
|
||||
]
|
||||
]
|
||||
|
||||
alert_notification alert_text _ =
|
||||
div [ class_ "alert alert-err" ] [ text alert_text ]
|
||||
|
||||
editField :: forall a. (a -> EditField) -> a -> BAction
|
||||
editField f = BEditField <<< f
|
||||
linkToFilterSingle slug = fromNullableStr app.userR <> "/b:" <> slug
|
||||
linkToFilterTag tag = fromNullableStr app.userR <> "/t:" <> encodeTag tag
|
||||
shdate = toLocaleDateString bm.time
|
||||
shdatetime = S.take 16 bm.time `append` "Z"
|
||||
|
||||
tagid bm = show bm.bid <> "_tags"
|
||||
|
||||
handleAction :: BAction -> H.HalogenM BState BAction ChildSlots BMessage Aff Unit
|
||||
|
||||
-- | Star
|
||||
handleAction (BStar e) = do
|
||||
bm <- use _bm
|
||||
H.liftAff (toggleStar bm.bid (if e then Star else UnStar))
|
||||
_bm %= _ { selected = e }
|
||||
_edit_bm %= _ { selected = e }
|
||||
|
||||
-- | Delete
|
||||
handleAction (BDeleteAsk e) = do
|
||||
H.modify_ (_ { deleteAsk = e })
|
||||
|
||||
-- | Destroy
|
||||
handleAction (BDestroy) = do
|
||||
bm <- use _bm
|
||||
void $ H.liftAff (destroy bm.bid)
|
||||
H.raise BNotifyRemove
|
||||
|
||||
-- | Mark Read
|
||||
handleAction (BMarkRead) = do
|
||||
bm <- use _bm
|
||||
void (H.liftAff (markRead bm.bid))
|
||||
_bm %= _ { toread = false }
|
||||
|
||||
-- | Start/Stop Editing
|
||||
handleAction (BEdit e) = do
|
||||
bm <- use _bm
|
||||
_edit_bm .= bm
|
||||
_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)
|
||||
|
|
|
@ -5,10 +5,9 @@ import Data.Const (Const)
|
|||
import Effect.Aff (Aff)
|
||||
import Foreign.Marked (marked)
|
||||
import Halogen as H
|
||||
import Halogen.HTML as HH
|
||||
import Prelude (Void)
|
||||
|
||||
type Slot = H.Slot (Const Void) Void
|
||||
|
||||
component :: forall q o. H.Component HH.HTML q String o Aff
|
||||
component :: forall q o. H.Component q String o Aff
|
||||
component = RH.mkComponent marked
|
||||
|
|
|
@ -12,7 +12,7 @@ import Globals (app', mmoment8601)
|
|||
import Halogen as H
|
||||
import Halogen.HTML (a, br_, div, text)
|
||||
import Halogen.HTML as HH
|
||||
import Halogen.HTML.Properties (href, id_, title)
|
||||
import Halogen.HTML.Properties (href, id, title)
|
||||
import Model (Note, NoteSlug)
|
||||
import Util (class_, fromNullableStr)
|
||||
|
||||
|
@ -27,7 +27,7 @@ type NLState =
|
|||
}
|
||||
|
||||
|
||||
nlist :: forall q i o. Array Note -> H.Component HH.HTML q i o Aff
|
||||
nlist :: forall q i o. Array Note -> H.Component q i o Aff
|
||||
nlist st' =
|
||||
H.mkComponent
|
||||
{ initialState: const (mkState st')
|
||||
|
@ -45,11 +45,11 @@ nlist st' =
|
|||
}
|
||||
|
||||
render :: NLState -> H.ComponentHTML NLAction () Aff
|
||||
render st@{ notes } =
|
||||
render { notes } =
|
||||
HH.div_ (map renderNote notes)
|
||||
where
|
||||
renderNote note =
|
||||
div [ id_ (show note.id)
|
||||
div [ id (show note.id)
|
||||
, class_ ("note w-100 mw7 pa1 mb2"
|
||||
<> if note.shared then "" else " private")] $
|
||||
[ div [ class_ "display" ] $
|
||||
|
|
|
@ -2,29 +2,35 @@ module Component.NNote where
|
|||
|
||||
import Prelude hiding (div)
|
||||
|
||||
import Affjax (printError)
|
||||
import Affjax.StatusCode (StatusCode(..))
|
||||
import App (destroyNote, editNote)
|
||||
import Component.Markdown as Markdown
|
||||
import Data.Array (drop, foldMap)
|
||||
import Data.Either (Either(..))
|
||||
import Data.Lens (Lens', lens, use, (%=), (.=))
|
||||
import Data.Maybe (Maybe(..), maybe)
|
||||
import Data.Maybe (Maybe(..), fromMaybe, isJust, maybe)
|
||||
import Data.Monoid (guard)
|
||||
import Data.String (null, split) as S
|
||||
import Data.String (null, stripPrefix)
|
||||
import Data.String.Pattern (Pattern(..))
|
||||
import Data.Tuple (fst, snd)
|
||||
import Effect.Aff (Aff)
|
||||
import Effect.Class (liftEffect)
|
||||
import Globals (app', mmoment8601)
|
||||
import Effect.Console (log)
|
||||
import Globals (app', mmoment8601, setFocus, closeWindow)
|
||||
import Halogen as H
|
||||
import Halogen.HTML (br_, button, div, form, input, label, p, span, text, textarea)
|
||||
import Halogen.HTML as HH
|
||||
import Halogen.HTML.Events (onChecked, onClick, onSubmit, onValueChange)
|
||||
import Halogen.HTML.Properties (ButtonType(..), InputType(..), checked, for, id_, name, rows, title, type_, value)
|
||||
import Halogen.HTML.Properties (ButtonType(..), InputType(..), autofocus, checked, for, id, name, rows, title, type_, value)
|
||||
import Model (Note)
|
||||
import Util (_loc, class_, fromNullableStr, ifElseH, whenH)
|
||||
import Type.Proxy (Proxy(..))
|
||||
import Util (_curQuerystring, _doc, _loc, _lookupQueryStringValue, class_, fromNullableStr, ifElseH, whenH)
|
||||
import Web.Event.Event (Event, preventDefault)
|
||||
import Web.HTML.Location (setHref)
|
||||
import Data.Symbol (SProxy(..))
|
||||
import Web.HTML (window)
|
||||
import Web.HTML.HTMLDocument (referrer)
|
||||
import Web.HTML.Location (origin, setHref)
|
||||
|
||||
data NAction
|
||||
= NNop
|
||||
|
@ -40,6 +46,7 @@ type NState =
|
|||
, deleteAsk :: Boolean
|
||||
, edit :: Boolean
|
||||
, destroyed :: Boolean
|
||||
, apiError :: Maybe String
|
||||
}
|
||||
|
||||
_note :: Lens' NState Note
|
||||
|
@ -51,6 +58,9 @@ _edit_note = lens _.edit_note (_ { edit_note = _ })
|
|||
_edit :: Lens' NState Boolean
|
||||
_edit = lens _.edit (_ { edit = _ })
|
||||
|
||||
_apiError :: Lens' NState (Maybe String)
|
||||
_apiError = lens _.apiError (_ { apiError = _ })
|
||||
|
||||
-- | FormField Edits
|
||||
data EditField
|
||||
= Etitle String
|
||||
|
@ -58,13 +68,13 @@ data EditField
|
|||
| EisMarkdown Boolean
|
||||
| Eshared Boolean
|
||||
|
||||
_markdown = SProxy :: SProxy "markdown"
|
||||
_markdown = Proxy :: Proxy "markdown"
|
||||
|
||||
type ChildSlots =
|
||||
( markdown :: Markdown.Slot Unit
|
||||
)
|
||||
|
||||
nnote :: forall q i o. Note -> H.Component HH.HTML q i o Aff
|
||||
nnote :: forall q i o. Note -> H.Component q i o Aff
|
||||
nnote st' =
|
||||
H.mkComponent
|
||||
{ initialState: const (mkState st')
|
||||
|
@ -80,10 +90,11 @@ nnote st' =
|
|||
, deleteAsk: false
|
||||
, edit: note'.id <= 0
|
||||
, destroyed: false
|
||||
, apiError: Nothing
|
||||
}
|
||||
|
||||
render :: NState -> H.ComponentHTML NAction ChildSlots Aff
|
||||
render st@{ note, edit_note } =
|
||||
render st@{ note, edit_note, apiError } =
|
||||
ifElseH st.destroyed
|
||||
display_destroyed
|
||||
(const (ifElseH st.edit
|
||||
|
@ -92,7 +103,7 @@ nnote st' =
|
|||
where
|
||||
|
||||
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_ ("link f5 lh-title")]
|
||||
[ text $ if S.null note.title then "[no title]" else note.title ]
|
||||
|
@ -110,12 +121,12 @@ nnote st' =
|
|||
]
|
||||
, whenH app.dat.isowner $ \_ ->
|
||||
div [ class_ "edit_links db mt3" ]
|
||||
[ button [ type_ ButtonButton, onClick \_ -> Just (NEdit true), class_ "edit light-silver hover-blue" ] [ text "edit " ]
|
||||
[ button [ type_ ButtonButton, onClick \_ -> NEdit true, class_ "edit light-silver hover-blue" ] [ text "edit " ]
|
||||
, div [ class_ "delete_link di" ]
|
||||
[ button [ type_ ButtonButton, onClick \_ -> Just (NDeleteAsk true), class_ ("delete light-silver hover-blue" <> guard st.deleteAsk " dn") ] [ text "delete" ]
|
||||
[ button [ type_ ButtonButton, onClick \_ -> NDeleteAsk true, class_ ("delete light-silver hover-blue" <> guard st.deleteAsk " dn") ] [ text "delete" ]
|
||||
, span ([ class_ ("confirm red" <> guard (not st.deleteAsk) " dn") ] )
|
||||
[ button [ type_ ButtonButton, onClick \_ -> Just (NDeleteAsk false)] [ text "cancel / " ]
|
||||
, button [ type_ ButtonButton, onClick \_ -> Just NDestroy, class_ "red" ] [ text "destroy" ]
|
||||
[ button [ type_ ButtonButton, onClick \_ -> NDeleteAsk false] [ text "cancel / " ]
|
||||
, button [ type_ ButtonButton, onClick \_ -> NDestroy, class_ "red" ] [ text "destroy" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
@ -123,25 +134,27 @@ nnote st' =
|
|||
]
|
||||
|
||||
renderNote_edit _ =
|
||||
form [ onSubmit (Just <<< NEditSubmit) ]
|
||||
[ p [ class_ "mt2 mb1"] [ text "title:" ]
|
||||
, input [ type_ InputText , class_ "title w-100 mb1 pt1 f7 edit_form_input" , name "title"
|
||||
, value (edit_note.title) , onValueChange (editField Etitle)
|
||||
form [ onSubmit NEditSubmit ]
|
||||
[ whenH (isJust apiError)
|
||||
(alert_notification (fromMaybe "" apiError))
|
||||
, p [ class_ "mt2 mb1"] [ text "title:" ]
|
||||
, 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_
|
||||
, p [ class_ "mt2 mb1"] [ text "description:" ]
|
||||
, textarea [ class_ "description w-100 mb1 pt1 f7 edit_form_input" , name "text", rows 30
|
||||
, textarea [ id (notetextid edit_note), class_ "description w-100 mb1 pt1 edit_form_input" , name "text", rows 25
|
||||
, value (edit_note.text) , onValueChange (editField Etext)
|
||||
]
|
||||
, div [ class_ "edit_form_checkboxes mb3"]
|
||||
[ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id_ "edit_ismarkdown", name "ismarkdown"
|
||||
[ input [ type_ InputCheckbox , class_ "is-markdown pointer" , id "edit_ismarkdown", name "ismarkdown"
|
||||
, checked (edit_note.isMarkdown) , onChecked (editField EisMarkdown) ]
|
||||
, text " "
|
||||
, label [ for "edit_ismarkdown" , class_ "mr2" ] [ text "use markdown?" ]
|
||||
, br_
|
||||
]
|
||||
, 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) ]
|
||||
, text " "
|
||||
, label [ for "edit_shared" , class_ "mr2" ] [ text "public?" ]
|
||||
|
@ -154,20 +167,24 @@ nnote st' =
|
|||
, input [ type_ InputReset
|
||||
, class_ "pv1 ph2 dark-gray ba b--moon-gray bg-near-white pointer rdim"
|
||||
, value "cancel"
|
||||
, onClick \_ -> Just (NEdit false)
|
||||
, onClick \_ -> NEdit false
|
||||
]
|
||||
]
|
||||
|
||||
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
|
||||
editField :: forall a. (a -> EditField) -> a -> Maybe NAction
|
||||
editField f = Just <<< NEditField <<< f
|
||||
editField :: forall a. (a -> EditField) -> a -> NAction
|
||||
editField f = NEditField <<< f
|
||||
toTextarea input =
|
||||
S.split (Pattern "\n") input
|
||||
# foldMap (\x -> [br_, text x])
|
||||
# drop 1
|
||||
|
||||
notetextid note = show note.id <> "_text"
|
||||
|
||||
handleAction :: NAction -> H.HalogenM NState NAction ChildSlots o Aff Unit
|
||||
handleAction (NNop) = pure unit
|
||||
|
@ -195,18 +212,38 @@ nnote st' =
|
|||
note <- use _note
|
||||
_edit_note .= note
|
||||
_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
|
||||
handleAction (NEditSubmit e) = do
|
||||
H.liftEffect (preventDefault e)
|
||||
edit_note <- use _edit_note
|
||||
res <- H.liftAff (editNote edit_note)
|
||||
case res.body of
|
||||
Left err -> pure unit
|
||||
Right r -> do
|
||||
if (edit_note.id == 0)
|
||||
then do
|
||||
liftEffect (setHref (fromNullableStr app.noteR) =<< _loc)
|
||||
else do
|
||||
_note .= edit_note
|
||||
_edit .= false
|
||||
_apiError .= Nothing
|
||||
H.liftAff (editNote edit_note) >>= case _ of
|
||||
Left affErr -> do
|
||||
_apiError .= Just (printError affErr)
|
||||
liftEffect $ log (printError affErr)
|
||||
Right { status: StatusCode s } | s >= 200 && s < 300 -> do
|
||||
qs <- liftEffect _curQuerystring
|
||||
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 $
|
||||
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)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// use at your own risk!
|
||||
exports.unsafeSetInnerHTML = function(element) {
|
||||
export const unsafeSetInnerHTML = function(element) {
|
||||
return function(html) {
|
||||
return function() {
|
||||
element.innerHTML = html;
|
||||
|
|
|
@ -19,6 +19,7 @@ data Action i
|
|||
= SetInnerHTML
|
||||
| Receive (Input i)
|
||||
|
||||
type Input :: forall k. k -> k
|
||||
type Input i = i
|
||||
|
||||
type State i =
|
||||
|
@ -26,10 +27,10 @@ type State i =
|
|||
, inputval :: Input i
|
||||
}
|
||||
|
||||
component :: forall q o. H.Component HH.HTML q (Input String) o Aff
|
||||
component :: forall q o. H.Component q (Input String) o Aff
|
||||
component = mkComponent RawHTML
|
||||
|
||||
mkComponent :: forall q i o. (Input i -> RawHTML) -> H.Component HH.HTML q (Input i) o Aff
|
||||
mkComponent :: forall q i o. (Input i -> RawHTML) -> H.Component q (Input i) o Aff
|
||||
mkComponent toRawHTML =
|
||||
H.mkComponent
|
||||
{ initialState: \inputval -> { elRef: H.RefLabel "inputval", inputval }
|
||||
|
@ -53,7 +54,7 @@ mkComponent toRawHTML =
|
|||
mel <- H.getHTMLElementRef elRef
|
||||
for_ mel \el -> do
|
||||
{ inputval } <- H.get
|
||||
H.liftAff $ forkAff $ makeAff \cb -> do
|
||||
H.liftAff $ forkAff $ makeAff \_ -> do
|
||||
liftEffect $ unsafeSetInnerHTML el (toRawHTML inputval)
|
||||
mempty
|
||||
pure unit
|
||||
|
|
173
purs/src/Component/TagCloud.purs
Normal file
173
purs/src/Component/TagCloud.purs
Normal file
|
@ -0,0 +1,173 @@
|
|||
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)
|
|
@ -1,67 +1,59 @@
|
|||
"use strict";
|
||||
|
||||
var moment = require("moment");
|
||||
|
||||
exports._app = function() {
|
||||
return app;
|
||||
}
|
||||
|
||||
exports._closest = function(just, nothing, selector, el) {
|
||||
var node = el.closest(selector);
|
||||
if(node) {
|
||||
return just(node);
|
||||
} else {
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
|
||||
exports._innerHtml = function(el) {
|
||||
return el.innerHTML;
|
||||
}
|
||||
|
||||
exports._setInnerHtml = function(content, el) {
|
||||
el.innerHTML = content;
|
||||
return el;
|
||||
}
|
||||
|
||||
exports._createFormData = function(formElement) {
|
||||
return new FormData(formElement);
|
||||
}
|
||||
|
||||
exports._createFormString = function(formElement) {
|
||||
return new URLSearchParams(new FormData(formElement)).toString()
|
||||
}
|
||||
|
||||
exports._createFormArray = function(formElement) {
|
||||
return Array.from(new FormData(formElement));
|
||||
}
|
||||
|
||||
exports._getDataAttribute = function(name, el) {
|
||||
return el.dataset[name];
|
||||
}
|
||||
|
||||
exports._setDataAttribute = function(name, value, el) {
|
||||
return el.dataset[name] = value;
|
||||
}
|
||||
|
||||
exports._moment8601 = function(tuple, s) {
|
||||
var m = moment(s, moment.ISO_8601);
|
||||
var s1 = m.fromNow();
|
||||
var s2 = m.format('MMMM D YYYY, h:mm a') + " (" + m.format() + ") ";
|
||||
return tuple(s1)(s2);
|
||||
}
|
||||
|
||||
exports._mmoment8601 = function(just, nothing, tuple, s) {
|
||||
try {
|
||||
var m = moment(s, moment.ISO_8601);
|
||||
var s1 = m.fromNow();
|
||||
var s2 = m.format('MMMM D YYYY, h:mm a') + " (" + m.format() + ") ";
|
||||
return just(tuple(s1)(s2));
|
||||
} catch (error) {
|
||||
return nothing
|
||||
}
|
||||
}
|
||||
|
||||
exports._closeWindow = function (window) {
|
||||
window.close();
|
||||
};
|
||||
"use strict";
|
||||
|
||||
import moment from 'moment'
|
||||
|
||||
export const _app = function() {
|
||||
return app;
|
||||
}
|
||||
|
||||
export const _closest = function(just, nothing, selector, el) {
|
||||
var node = el.closest(selector);
|
||||
if(node) {
|
||||
return just(node);
|
||||
} else {
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
|
||||
export const _createFormData = function(formElement) {
|
||||
return new FormData(formElement);
|
||||
}
|
||||
|
||||
export const _createFormString = function(formElement) {
|
||||
return new URLSearchParams(new FormData(formElement)).toString()
|
||||
}
|
||||
|
||||
export const _createFormArray = function(formElement) {
|
||||
return Array.from(new FormData(formElement));
|
||||
}
|
||||
|
||||
export const _moment8601 = function(tuple, s) {
|
||||
var m = moment(s, moment.ISO_8601);
|
||||
var s1 = m.fromNow();
|
||||
var s2 = m.format('MMMM D YYYY, h:mm a') + " (" + m.format() + ") ";
|
||||
return tuple(s1)(s2);
|
||||
}
|
||||
|
||||
export const _mmoment8601 = function(just, nothing, tuple, s) {
|
||||
try {
|
||||
var m = moment(s, moment.ISO_8601);
|
||||
var s1 = m.fromNow();
|
||||
var s2 = m.format('MMMM D YYYY, h:mm a') + " (" + m.format() + ") ";
|
||||
return just(tuple(s1)(s2));
|
||||
} catch (error) {
|
||||
return nothing
|
||||
}
|
||||
}
|
||||
|
||||
export const _closeWindow = function (window) {
|
||||
window.close();
|
||||
};
|
||||
|
||||
export const _setFocus = function(elemId) {
|
||||
document.getElementById(elemId).focus();
|
||||
};
|
||||
|
||||
|
||||
export const _toLocaleDateString = function(dateString) {
|
||||
return new Date(dateString).toLocaleDateString(undefined, {dateStyle: 'medium'})
|
||||
}
|
||||
|
|
|
@ -1,97 +1,87 @@
|
|||
module Globals where
|
||||
|
||||
import Data.Function.Uncurried
|
||||
|
||||
import Data.Maybe (Maybe(..))
|
||||
import Data.Nullable (Nullable, toMaybe)
|
||||
import Data.Tuple (Tuple(..))
|
||||
import Effect (Effect)
|
||||
import Model (Bookmark)
|
||||
import Prelude (Unit, pure, ($))
|
||||
import Web.DOM (Element, Node)
|
||||
import Web.HTML (HTMLElement, HTMLFormElement, Window)
|
||||
import Web.XHR.FormData (FormData)
|
||||
import Data.Newtype (class Newtype)
|
||||
|
||||
type App =
|
||||
{ csrfHeaderName :: String
|
||||
, csrfCookieName :: String
|
||||
, csrfParamName :: String
|
||||
, csrfToken :: String
|
||||
, homeR :: String
|
||||
, authRlogoutR :: String
|
||||
, userR :: Nullable String
|
||||
, noteR :: Nullable String
|
||||
, dat :: AppData
|
||||
}
|
||||
|
||||
type AppData =
|
||||
{ bmarks :: Array Bookmark
|
||||
, bmark :: Bookmark
|
||||
, isowner :: Boolean
|
||||
}
|
||||
|
||||
foreign import _app :: Fn0 App
|
||||
|
||||
app' :: Unit -> App
|
||||
app' _ = runFn0 _app
|
||||
|
||||
foreign import _closest :: forall a. Fn4 (a -> Maybe a) (Maybe a) String Node (Maybe Node)
|
||||
|
||||
closest :: String -> Node -> Effect (Maybe Node)
|
||||
closest selector node = pure $ runFn4 _closest Just Nothing selector node
|
||||
|
||||
foreign import _moment8601 :: Fn2 (String -> String -> Tuple String String) String (Tuple String String)
|
||||
|
||||
moment8601 :: String -> Effect (Tuple String String)
|
||||
moment8601 s = pure $ runFn2 _moment8601 Tuple s
|
||||
|
||||
foreign import _mmoment8601 :: forall a. Fn4 (a -> Maybe a) (Maybe a) (String -> String -> Tuple String String) String (Maybe (Tuple String String))
|
||||
|
||||
mmoment8601 :: String -> Maybe (Tuple String String)
|
||||
mmoment8601 s = runFn4 _mmoment8601 Just Nothing Tuple s
|
||||
|
||||
foreign import _innerHtml :: Fn1 HTMLElement String
|
||||
|
||||
innerHtml :: HTMLElement -> Effect String
|
||||
innerHtml n = pure $ runFn1 _innerHtml n
|
||||
|
||||
foreign import _setInnerHtml :: Fn2 String HTMLElement HTMLElement
|
||||
|
||||
setInnerHtml :: String -> HTMLElement -> Effect HTMLElement
|
||||
setInnerHtml c n = pure $ runFn2 _setInnerHtml c n
|
||||
|
||||
foreign import _createFormData :: Fn1 HTMLFormElement FormData
|
||||
|
||||
createFormData :: HTMLFormElement -> FormData
|
||||
createFormData f = runFn1 _createFormData f
|
||||
|
||||
foreign import _createFormString :: Fn1 HTMLFormElement String
|
||||
|
||||
createFormString :: HTMLFormElement -> String
|
||||
createFormString f = runFn1 _createFormString f
|
||||
|
||||
|
||||
foreign import _createFormArray :: Fn1 HTMLFormElement (Array (Array String))
|
||||
|
||||
createFormArray :: HTMLFormElement -> (Array (Array String))
|
||||
createFormArray f = runFn1 _createFormArray f
|
||||
|
||||
foreign import _getDataAttribute :: Fn2 String Element (Nullable String)
|
||||
|
||||
getDataAttribute :: String -> Element -> Effect (Maybe String)
|
||||
getDataAttribute k n = pure $ toMaybe $ runFn2 _getDataAttribute k n
|
||||
|
||||
foreign import _setDataAttribute :: Fn3 String String Element Unit
|
||||
|
||||
setDataAttribute :: String -> String -> Element -> Effect Unit
|
||||
setDataAttribute k v n = pure $ runFn3 _setDataAttribute k v n
|
||||
|
||||
foreign import _closeWindow :: Fn1 Window Unit
|
||||
|
||||
closeWindow :: Window -> Effect Unit
|
||||
closeWindow win = pure $ runFn1 _closeWindow win
|
||||
|
||||
newtype RawHTML = RawHTML String
|
||||
|
||||
derive instance newtypeRawHTML :: Newtype RawHTML _
|
||||
module Globals where
|
||||
|
||||
import Data.Maybe (Maybe(..))
|
||||
import Data.Newtype (class Newtype)
|
||||
import Data.Nullable (Nullable)
|
||||
import Data.Tuple (Tuple(..))
|
||||
import Effect (Effect)
|
||||
import Data.Function.Uncurried (Fn0, Fn1, Fn4, runFn0, runFn1, runFn4)
|
||||
import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn4, runEffectFn1, runEffectFn2, runEffectFn4)
|
||||
import Model (Bookmark)
|
||||
import Prelude (Unit)
|
||||
import Web.DOM (Node)
|
||||
import Web.HTML (HTMLFormElement, Window)
|
||||
import Web.XHR.FormData (FormData)
|
||||
|
||||
type App =
|
||||
{ csrfHeaderName :: String
|
||||
, csrfCookieName :: String
|
||||
, csrfParamName :: String
|
||||
, csrfToken :: String
|
||||
, homeR :: String
|
||||
, authRlogoutR :: String
|
||||
, userR :: Nullable String
|
||||
, noteR :: Nullable String
|
||||
, dat :: AppData
|
||||
}
|
||||
|
||||
type AppData =
|
||||
{ bmarks :: Array Bookmark
|
||||
, bmark :: Bookmark
|
||||
, isowner :: Boolean
|
||||
}
|
||||
|
||||
foreign import _app :: Fn0 App
|
||||
|
||||
app' :: Unit -> App
|
||||
app' _ = runFn0 _app
|
||||
|
||||
foreign import _closest :: forall a. EffectFn4 (a -> Maybe a) (Maybe a) String Node (Maybe Node)
|
||||
|
||||
closest :: String -> Node -> Effect (Maybe Node)
|
||||
closest selector node = runEffectFn4 _closest Just Nothing selector node
|
||||
|
||||
foreign import _moment8601 :: EffectFn2 (String -> String -> Tuple String String) String (Tuple String String)
|
||||
|
||||
moment8601 :: String -> Effect (Tuple String String)
|
||||
moment8601 s = runEffectFn2 _moment8601 Tuple s
|
||||
|
||||
foreign import _mmoment8601 :: forall a. Fn4 (a -> Maybe a) (Maybe a) (String -> String -> Tuple String String) String (Maybe (Tuple String String))
|
||||
|
||||
mmoment8601 :: String -> Maybe (Tuple String String)
|
||||
mmoment8601 s = runFn4 _mmoment8601 Just Nothing Tuple s
|
||||
|
||||
foreign import _createFormData :: Fn1 HTMLFormElement FormData
|
||||
|
||||
createFormData :: HTMLFormElement -> FormData
|
||||
createFormData f = runFn1 _createFormData f
|
||||
|
||||
foreign import _createFormString :: Fn1 HTMLFormElement String
|
||||
|
||||
createFormString :: HTMLFormElement -> String
|
||||
createFormString f = runFn1 _createFormString f
|
||||
|
||||
|
||||
foreign import _createFormArray :: Fn1 HTMLFormElement (Array (Array String))
|
||||
|
||||
createFormArray :: HTMLFormElement -> (Array (Array String))
|
||||
createFormArray f = runFn1 _createFormArray f
|
||||
|
||||
foreign import _closeWindow :: EffectFn1 Window Unit
|
||||
|
||||
closeWindow :: Window -> Effect Unit
|
||||
closeWindow win = runEffectFn1 _closeWindow win
|
||||
|
||||
newtype RawHTML = RawHTML String
|
||||
|
||||
derive instance newtypeRawHTML :: Newtype RawHTML _
|
||||
|
||||
foreign import _setFocus :: EffectFn1 String Unit
|
||||
|
||||
setFocus :: String -> Effect Unit
|
||||
setFocus s = runEffectFn1 _setFocus s
|
||||
|
||||
foreign import _toLocaleDateString :: Fn1 String String
|
||||
|
||||
toLocaleDateString :: String -> String
|
||||
toLocaleDateString s = runFn1 _toLocaleDateString s
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
exports._mainImpl = function() {
|
||||
return window.PS = PS;
|
||||
}
|
|
@ -1,68 +1,70 @@
|
|||
module Main where
|
||||
|
||||
import Prelude
|
||||
|
||||
import App (logout)
|
||||
import Component.Add (addbmark)
|
||||
import Component.BList (blist)
|
||||
import Component.NList (nlist)
|
||||
import Component.NNote (nnote)
|
||||
import Component.AccountSettings (usetting)
|
||||
import Data.Foldable (traverse_)
|
||||
import Effect (Effect)
|
||||
import Effect.Aff (Aff, launchAff)
|
||||
import Effect.Class (liftEffect)
|
||||
import Halogen.Aff as HA
|
||||
import Halogen.VDom.Driver (runUI)
|
||||
import Model (Bookmark, Note, AccountSettings)
|
||||
import Web.DOM.Element (setAttribute)
|
||||
import Web.DOM.ParentNode (QuerySelector(..))
|
||||
import Web.Event.Event (Event, preventDefault)
|
||||
import Web.HTML.HTMLElement (toElement)
|
||||
|
||||
foreign import _mainImpl :: Effect Unit
|
||||
|
||||
main :: Effect Unit
|
||||
main = _mainImpl
|
||||
|
||||
logoutE :: Event -> Effect Unit
|
||||
logoutE e = void <<< launchAff <<< logout =<< preventDefault e
|
||||
|
||||
renderBookmarks :: String -> Array Bookmark -> Effect Unit
|
||||
renderBookmarks renderElSelector bmarks = do
|
||||
HA.runHalogenAff do
|
||||
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||
void $ runUI (blist bmarks) unit el
|
||||
viewRendered
|
||||
|
||||
renderAddForm :: String -> Bookmark -> Effect Unit
|
||||
renderAddForm renderElSelector bmark = do
|
||||
HA.runHalogenAff do
|
||||
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||
void $ runUI (addbmark bmark) unit el
|
||||
viewRendered
|
||||
|
||||
renderNotes :: String -> Array Note -> Effect Unit
|
||||
renderNotes renderElSelector notes = do
|
||||
HA.runHalogenAff do
|
||||
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||
void $ runUI (nlist notes) unit el
|
||||
viewRendered
|
||||
|
||||
renderNote :: String -> Note -> Effect Unit
|
||||
renderNote renderElSelector note = do
|
||||
HA.runHalogenAff do
|
||||
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||
void $ runUI (nnote note) unit el
|
||||
viewRendered
|
||||
|
||||
renderAccountSettings :: String -> AccountSettings -> Effect Unit
|
||||
renderAccountSettings renderElSelector accountSettings = do
|
||||
HA.runHalogenAff do
|
||||
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||
void $ runUI (usetting accountSettings) unit el
|
||||
viewRendered
|
||||
|
||||
viewRendered :: Aff Unit
|
||||
viewRendered = HA.selectElement (QuerySelector "#content") >>= traverse_ \el ->
|
||||
liftEffect $ setAttribute "view-rendered" "" (toElement el)
|
||||
module Main where
|
||||
|
||||
import Prelude
|
||||
|
||||
import App (logout)
|
||||
import Component.AccountSettings (usetting)
|
||||
import Component.Add (addbmark)
|
||||
import Component.BList (blist)
|
||||
import Component.NList (nlist)
|
||||
import Component.NNote (nnote)
|
||||
import Component.TagCloud (tagcloudcomponent)
|
||||
import Data.Foldable (traverse_)
|
||||
import Effect (Effect)
|
||||
import Effect.Aff (Aff, launchAff)
|
||||
import Effect.Class (liftEffect)
|
||||
import Halogen.Aff as HA
|
||||
import Halogen.VDom.Driver (runUI)
|
||||
import Model (AccountSettings, Bookmark, Note, TagCloudMode, tagCloudModeToF)
|
||||
import Web.DOM.Element (setAttribute)
|
||||
import Web.DOM.ParentNode (QuerySelector(..))
|
||||
import Web.Event.Event (Event, preventDefault)
|
||||
import Web.HTML.HTMLElement (toElement)
|
||||
|
||||
logoutE :: Event -> Effect Unit
|
||||
logoutE e = void <<< launchAff <<< logout =<< preventDefault e
|
||||
|
||||
renderBookmarks :: String -> Array Bookmark -> Effect Unit
|
||||
renderBookmarks renderElSelector bmarks = do
|
||||
HA.runHalogenAff do
|
||||
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||
void $ runUI (blist bmarks) unit el
|
||||
viewRendered
|
||||
|
||||
renderTagCloud :: String -> TagCloudMode -> Effect Unit
|
||||
renderTagCloud renderElSelector tagCloudMode = do
|
||||
HA.runHalogenAff do
|
||||
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||
void $ runUI (tagcloudcomponent (tagCloudModeToF tagCloudMode)) unit el
|
||||
|
||||
renderAddForm :: String -> Bookmark -> Effect Unit
|
||||
renderAddForm renderElSelector bmark = do
|
||||
HA.runHalogenAff do
|
||||
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||
void $ runUI (addbmark bmark) unit el
|
||||
viewRendered
|
||||
|
||||
renderNotes :: String -> Array Note -> Effect Unit
|
||||
renderNotes renderElSelector notes = do
|
||||
HA.runHalogenAff do
|
||||
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||
void $ runUI (nlist notes) unit el
|
||||
viewRendered
|
||||
|
||||
renderNote :: String -> Note -> Effect Unit
|
||||
renderNote renderElSelector note = do
|
||||
HA.runHalogenAff do
|
||||
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||
void $ runUI (nnote note) unit el
|
||||
viewRendered
|
||||
|
||||
renderAccountSettings :: String -> AccountSettings -> Effect Unit
|
||||
renderAccountSettings renderElSelector accountSettings = do
|
||||
HA.runHalogenAff do
|
||||
HA.selectElement (QuerySelector renderElSelector) >>= traverse_ \el -> do
|
||||
void $ runUI (usetting accountSettings) unit el
|
||||
viewRendered
|
||||
|
||||
viewRendered :: Aff Unit
|
||||
viewRendered = HA.selectElement (QuerySelector "#content") >>= traverse_ \el ->
|
||||
liftEffect $ setAttribute "view-rendered" "" (toElement el)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
var marked = require("marked");
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from "dompurify"
|
||||
|
||||
marked.setOptions({
|
||||
pedantic: false,
|
||||
gfm: true
|
||||
});
|
||||
|
||||
exports.markedImpl = function(str) {
|
||||
export const markedImpl = function(str) {
|
||||
if (!str) return "";
|
||||
return marked(str);
|
||||
return DOMPurify.sanitize(marked.parse(str));
|
||||
};
|
||||
|
|
|
@ -1,54 +1,129 @@
|
|||
module Model where
|
||||
|
||||
import Data.Nullable (Nullable)
|
||||
import Simple.JSON as J
|
||||
|
||||
type BookmarkId = Int
|
||||
type TagId = Int
|
||||
|
||||
type Bookmark =
|
||||
{ url :: String
|
||||
, title :: String
|
||||
, description :: String
|
||||
, tags :: String
|
||||
, private :: Boolean
|
||||
, toread :: Boolean
|
||||
, bid :: BookmarkId
|
||||
, slug :: String
|
||||
, selected :: Boolean
|
||||
, time :: String
|
||||
, archiveUrl :: Nullable String
|
||||
}
|
||||
|
||||
newtype Bookmark' = Bookmark' Bookmark
|
||||
derive newtype instance bookmark_rfI :: J.ReadForeign Bookmark'
|
||||
derive newtype instance bookmark_wfI :: J.WriteForeign Bookmark'
|
||||
|
||||
type NoteId = Int
|
||||
type NoteSlug = String
|
||||
|
||||
type Note =
|
||||
{ id :: NoteId
|
||||
, slug :: NoteSlug
|
||||
, title :: String
|
||||
, text :: String
|
||||
, length :: Int
|
||||
, isMarkdown :: Boolean
|
||||
, shared :: Boolean
|
||||
, created :: String
|
||||
, updated :: String
|
||||
}
|
||||
|
||||
newtype Note' = Note' Note
|
||||
derive newtype instance note_rfI :: J.ReadForeign Note'
|
||||
derive newtype instance note_wfI :: J.WriteForeign Note'
|
||||
|
||||
type AccountSettings =
|
||||
{ archiveDefault :: Boolean
|
||||
, privateDefault :: Boolean
|
||||
, privacyLock :: Boolean
|
||||
}
|
||||
|
||||
newtype AccountSettings' = AccountSettings' AccountSettings
|
||||
derive newtype instance usersettings_rfI :: J.ReadForeign AccountSettings'
|
||||
derive newtype instance usersettings_wfI :: J.WriteForeign AccountSettings'
|
||||
module Model where
|
||||
|
||||
import Control.Monad.Except (runExcept)
|
||||
import Data.Array (intercalate)
|
||||
import Data.Either (hush)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Nullable (Nullable)
|
||||
import Data.String (Pattern(..), split)
|
||||
import Foreign (Foreign, readInt, readString, unsafeToForeign)
|
||||
import Foreign.Object (Object)
|
||||
import Prelude (class Eq, pure, ($), (<$>))
|
||||
import Simple.JSON as J
|
||||
|
||||
type BookmarkId = Int
|
||||
type TagId = Int
|
||||
|
||||
type Bookmark =
|
||||
{ url :: String
|
||||
, title :: String
|
||||
, description :: String
|
||||
, tags :: String
|
||||
, private :: Boolean
|
||||
, toread :: Boolean
|
||||
, bid :: BookmarkId
|
||||
, slug :: String
|
||||
, selected :: Boolean
|
||||
, time :: String
|
||||
, archiveUrl :: Nullable String
|
||||
}
|
||||
|
||||
newtype Bookmark' = Bookmark' Bookmark
|
||||
derive newtype instance J.ReadForeign Bookmark'
|
||||
derive newtype instance J.WriteForeign Bookmark'
|
||||
|
||||
type NoteId = Int
|
||||
type NoteSlug = String
|
||||
|
||||
type Note =
|
||||
{ id :: NoteId
|
||||
, slug :: NoteSlug
|
||||
, title :: String
|
||||
, text :: String
|
||||
, length :: Int
|
||||
, isMarkdown :: Boolean
|
||||
, shared :: Boolean
|
||||
, created :: String
|
||||
, updated :: String
|
||||
}
|
||||
|
||||
newtype Note' = Note' Note
|
||||
derive newtype instance J.ReadForeign Note'
|
||||
derive newtype instance J.WriteForeign Note'
|
||||
|
||||
type 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
|
||||
|
|
|
@ -1,142 +1,155 @@
|
|||
module Util where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Control.Monad.Maybe.Trans (MaybeT(..))
|
||||
import Data.Array (filter, find, mapMaybe)
|
||||
import Data.Foldable (for_)
|
||||
import Data.Maybe (Maybe(..), fromJust, fromMaybe, maybe)
|
||||
import Data.Nullable (Nullable, toMaybe)
|
||||
import Data.String (Pattern(..), Replacement(..), drop, replaceAll, split, take)
|
||||
import Data.Tuple (Tuple(..), fst, snd)
|
||||
import Effect (Effect)
|
||||
import Global.Unsafe (unsafeDecodeURIComponent)
|
||||
import Halogen (ClassName(..))
|
||||
import Halogen.HTML as HH
|
||||
import Halogen.HTML.Properties as HP
|
||||
import Partial.Unsafe (unsafePartial)
|
||||
import Web.DOM (Element, Node)
|
||||
import Web.DOM.Document (toNonElementParentNode)
|
||||
import Web.DOM.Element (fromNode, toParentNode)
|
||||
import Web.DOM.NodeList (toArray)
|
||||
import Web.DOM.NonElementParentNode (getElementById)
|
||||
import Web.DOM.ParentNode (QuerySelector(..), querySelector, querySelectorAll)
|
||||
import Web.HTML (HTMLDocument, Location, window)
|
||||
import Web.HTML.HTMLDocument (body) as HD
|
||||
import Web.HTML.HTMLDocument (toDocument)
|
||||
import Web.HTML.HTMLElement (HTMLElement)
|
||||
import Web.HTML.HTMLElement (fromElement) as HE
|
||||
import Web.HTML.Location (search)
|
||||
import Web.HTML.Window (document, location)
|
||||
|
||||
-- Halogen
|
||||
|
||||
class_ :: forall r i. String -> HP.IProp ( "class" :: String | r) i
|
||||
class_ = HP.class_ <<< HH.ClassName
|
||||
|
||||
attr :: forall r i. String -> String -> HP.IProp r i
|
||||
attr a = HP.attr (HH.AttrName a)
|
||||
|
||||
-- Util
|
||||
|
||||
_queryBoth :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Element -> Effect a) -> Effect Unit
|
||||
_queryBoth (Tuple qa ea) (Tuple qb eb) f = do
|
||||
ma <- _querySelector qa ea
|
||||
mb <- _querySelector qb eb
|
||||
for_ ma \a ->
|
||||
for_ mb \b ->
|
||||
f a b
|
||||
|
||||
_queryBoth' :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Array Node -> Effect a) -> Effect Unit
|
||||
_queryBoth' (Tuple qa ea) (Tuple qb eb) f = do
|
||||
ma <- _querySelector qa ea
|
||||
bs <- _querySelectorAll qb eb
|
||||
for_ ma \a ->
|
||||
f a bs
|
||||
|
||||
_queryBoth'' :: forall a. Tuple String Element -> Tuple String Element -> (Array Node -> Array Node -> Effect a) -> Effect a
|
||||
_queryBoth'' (Tuple qa ea) (Tuple qb eb) f = do
|
||||
as <- _querySelectorAll qa ea
|
||||
bs <- _querySelectorAll qb eb
|
||||
f as bs
|
||||
|
||||
_querySelector :: String -> Element -> Effect (Maybe Element)
|
||||
_querySelector s n = querySelector (QuerySelector s) (toParentNode n)
|
||||
|
||||
_querySelectorAll :: String -> Element -> Effect (Array Node)
|
||||
_querySelectorAll s n = toArray =<< querySelectorAll (QuerySelector s) (toParentNode n)
|
||||
|
||||
_fromNode :: Node -> Element
|
||||
_fromNode e = unsafePartial $ fromJust (fromNode e)
|
||||
|
||||
_fromElement :: Element -> HTMLElement
|
||||
_fromElement e = unsafePartial $ fromJust (HE.fromElement e)
|
||||
|
||||
_getElementById :: String -> HTMLDocument -> Effect (Maybe Element)
|
||||
_getElementById s = getElementById s <<< toNonElementParentNode <<< toDocument
|
||||
|
||||
_doc :: Effect HTMLDocument
|
||||
_doc = document =<< window
|
||||
|
||||
_loc :: Effect Location
|
||||
_loc = location =<< window
|
||||
|
||||
type QueryStringArray = Array (Tuple String (Maybe String))
|
||||
|
||||
_curQuerystring :: Effect QueryStringArray
|
||||
_curQuerystring = do
|
||||
loc <- _loc
|
||||
srh <- search loc
|
||||
pure $ _parseQueryString srh
|
||||
|
||||
_parseQueryString :: String -> QueryStringArray
|
||||
_parseQueryString srh = do
|
||||
let qs = let srh' = take 1 srh in if (srh' == "#" || srh' == "?") then drop 1 srh else srh
|
||||
mapMaybe go $ (filter (_ /= "") <<< split (Pattern "&")) qs
|
||||
where
|
||||
decode = unsafeDecodeURIComponent <<< replaceAll (Pattern "+") (Replacement " ")
|
||||
go kv =
|
||||
case split (Pattern "=") kv of
|
||||
[k] -> Just (Tuple (decode k) Nothing)
|
||||
[k, v] -> Just (Tuple (decode k) (Just (decode v)))
|
||||
_ -> Nothing
|
||||
|
||||
_lookupQueryStringValue :: QueryStringArray -> String -> Maybe String
|
||||
_lookupQueryStringValue qs k = do
|
||||
join $ map snd $ find ((_ == k) <<< fst) qs
|
||||
|
||||
_body :: Effect HTMLElement
|
||||
_body = unsafePartial $ pure <<< fromJust =<< HD.body =<< _doc
|
||||
|
||||
_mt :: forall a. Effect (Maybe a) -> MaybeT Effect a
|
||||
_mt = MaybeT
|
||||
|
||||
_mt_pure :: forall a. Maybe a -> MaybeT Effect a
|
||||
_mt_pure = MaybeT <<< pure
|
||||
|
||||
dummyAttr :: forall r i. HP.IProp r i
|
||||
dummyAttr = HP.attr (HH.AttrName "data-dummy") ""
|
||||
|
||||
whenP :: forall r i. Boolean -> HP.IProp r i -> HP.IProp r i
|
||||
whenP b p = if b then p else dummyAttr
|
||||
|
||||
maybeP :: forall a r i. Maybe a -> (a -> HP.IProp r i) -> HP.IProp r i
|
||||
maybeP m p = maybe dummyAttr p m
|
||||
|
||||
whenC :: Boolean -> ClassName -> ClassName
|
||||
whenC b c = if b then c else ClassName ""
|
||||
|
||||
whenH :: forall p i. Boolean -> (Unit -> HH.HTML p i) -> HH.HTML p i
|
||||
whenH b k = if b then k unit else HH.text ""
|
||||
|
||||
whenA :: forall t. Boolean -> (Unit -> Array t) -> Array t
|
||||
whenA b k = if b then k unit else []
|
||||
|
||||
ifElseH :: forall p i. Boolean -> (Unit -> HH.HTML p i) -> (Unit -> HH.HTML p i) -> HH.HTML p i
|
||||
ifElseH 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
|
||||
module Util where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Control.Monad.Maybe.Trans (MaybeT(..))
|
||||
import Data.Array (filter, find, mapMaybe)
|
||||
import Data.Foldable (for_)
|
||||
import Data.Maybe (Maybe(..), fromJust, fromMaybe, maybe)
|
||||
import Data.Nullable (Nullable, toMaybe)
|
||||
import Data.String (Pattern(..), Replacement(..), drop, replaceAll, split, take)
|
||||
import Data.String as S
|
||||
import Data.Tuple (Tuple(..), fst, snd)
|
||||
import Effect (Effect)
|
||||
import Halogen (ClassName(..))
|
||||
import Halogen.HTML as HH
|
||||
import Halogen.HTML.Properties as HP
|
||||
import JSURI (decodeURIComponent, encodeURIComponent)
|
||||
import Partial.Unsafe (unsafePartial)
|
||||
import Web.DOM (Element, Node)
|
||||
import Web.DOM.Document (toNonElementParentNode)
|
||||
import Web.DOM.Element (fromNode, toParentNode)
|
||||
import Web.DOM.NodeList (toArray)
|
||||
import Web.DOM.NonElementParentNode (getElementById)
|
||||
import Web.DOM.ParentNode (QuerySelector(..), querySelector, querySelectorAll)
|
||||
import Web.HTML (HTMLDocument, Location, window)
|
||||
import Web.HTML.HTMLDocument (body) as HD
|
||||
import Web.HTML.HTMLDocument (toDocument)
|
||||
import Web.HTML.HTMLElement (HTMLElement)
|
||||
import Web.HTML.HTMLElement (fromElement) as HE
|
||||
import Web.HTML.Location (search)
|
||||
import Web.HTML.Window (document, location)
|
||||
|
||||
unsafeDecode :: String -> String
|
||||
unsafeDecode str = unsafePartial $ fromJust $ decodeURIComponent str
|
||||
|
||||
-- Halogen
|
||||
|
||||
class_ :: forall r i. String -> HP.IProp ( "class" :: String | r) i
|
||||
class_ = HP.class_ <<< HH.ClassName
|
||||
|
||||
attr :: forall r i. String -> String -> HP.IProp r i
|
||||
attr a = HP.attr (HH.AttrName a)
|
||||
|
||||
-- Util
|
||||
|
||||
_queryBoth :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Element -> Effect a) -> Effect Unit
|
||||
_queryBoth (Tuple qa ea) (Tuple qb eb) f = do
|
||||
ma <- _querySelector qa ea
|
||||
mb <- _querySelector qb eb
|
||||
for_ ma \a ->
|
||||
for_ mb \b ->
|
||||
f a b
|
||||
|
||||
_queryBoth' :: forall a. Tuple String Element -> Tuple String Element -> (Element -> Array Node -> Effect a) -> Effect Unit
|
||||
_queryBoth' (Tuple qa ea) (Tuple qb eb) f = do
|
||||
ma <- _querySelector qa ea
|
||||
bs <- _querySelectorAll qb eb
|
||||
for_ ma \a ->
|
||||
f a bs
|
||||
|
||||
_queryBoth'' :: forall a. Tuple String Element -> Tuple String Element -> (Array Node -> Array Node -> Effect a) -> Effect a
|
||||
_queryBoth'' (Tuple qa ea) (Tuple qb eb) f = do
|
||||
as <- _querySelectorAll qa ea
|
||||
bs <- _querySelectorAll qb eb
|
||||
f as bs
|
||||
|
||||
_querySelector :: String -> Element -> Effect (Maybe Element)
|
||||
_querySelector s n = querySelector (QuerySelector s) (toParentNode n)
|
||||
|
||||
_querySelectorAll :: String -> Element -> Effect (Array Node)
|
||||
_querySelectorAll s n = toArray =<< querySelectorAll (QuerySelector s) (toParentNode n)
|
||||
|
||||
_fromNode :: Node -> Element
|
||||
_fromNode e = unsafePartial $ fromJust (fromNode e)
|
||||
|
||||
_fromElement :: Element -> HTMLElement
|
||||
_fromElement e = unsafePartial $ fromJust (HE.fromElement e)
|
||||
|
||||
_getElementById :: String -> HTMLDocument -> Effect (Maybe Element)
|
||||
_getElementById s = getElementById s <<< toNonElementParentNode <<< toDocument
|
||||
|
||||
_doc :: Effect HTMLDocument
|
||||
_doc = document =<< window
|
||||
|
||||
_loc :: Effect Location
|
||||
_loc = location =<< window
|
||||
|
||||
type QueryStringArray = Array (Tuple String (Maybe String))
|
||||
|
||||
_curQuerystring :: Effect QueryStringArray
|
||||
_curQuerystring = do
|
||||
loc <- _loc
|
||||
srh <- search loc
|
||||
pure $ _parseQueryString srh
|
||||
|
||||
_parseQueryString :: String -> QueryStringArray
|
||||
_parseQueryString srh = do
|
||||
let qs = let srh' = take 1 srh in if (srh' == "#" || srh' == "?") then drop 1 srh else srh
|
||||
mapMaybe go $ (filter (_ /= "") <<< split (Pattern "&")) qs
|
||||
where
|
||||
decode = unsafeDecode <<< replaceAll (Pattern "+") (Replacement " ")
|
||||
go kv =
|
||||
case split (Pattern "=") kv of
|
||||
[k] -> Just (Tuple (decode k) Nothing)
|
||||
[k, v] -> Just (Tuple (decode k) (Just (decode v)))
|
||||
_ -> Nothing
|
||||
|
||||
_lookupQueryStringValue :: QueryStringArray -> String -> Maybe String
|
||||
_lookupQueryStringValue qs k = do
|
||||
join $ map snd $ find ((_ == k) <<< fst) qs
|
||||
|
||||
_body :: Effect HTMLElement
|
||||
_body = unsafePartial $ pure <<< fromJust =<< HD.body =<< _doc
|
||||
|
||||
_mt :: forall a. Effect (Maybe a) -> MaybeT Effect a
|
||||
_mt = MaybeT
|
||||
|
||||
_mt_pure :: forall a. Maybe a -> MaybeT Effect a
|
||||
_mt_pure = MaybeT <<< pure
|
||||
|
||||
encodeTag :: String -> String
|
||||
encodeTag = fromMaybe "" <<< encodeURIComponent <<< replaceAll (Pattern "+") (Replacement "%2B")
|
||||
|
||||
dummyAttr :: forall r i. HP.IProp r i
|
||||
dummyAttr = HP.attr (HH.AttrName "data-dummy") ""
|
||||
|
||||
whenP :: forall r i. Boolean -> HP.IProp r i -> HP.IProp r i
|
||||
whenP b p = if b then p else dummyAttr
|
||||
|
||||
maybeP :: forall a r i. Maybe a -> (a -> HP.IProp r i) -> HP.IProp r i
|
||||
maybeP m p = maybe dummyAttr p m
|
||||
|
||||
whenC :: Boolean -> ClassName -> ClassName
|
||||
whenC b c = if b then c else ClassName ""
|
||||
|
||||
whenH :: forall p i. Boolean -> (Unit -> HH.HTML p i) -> HH.HTML p i
|
||||
whenH b k = if b then k unit else HH.text ""
|
||||
|
||||
whenA :: forall t. Boolean -> (Unit -> Array t) -> Array t
|
||||
whenA b k = if b then k unit else []
|
||||
|
||||
ifElseH :: forall p i. Boolean -> (Unit -> HH.HTML p i) -> (Unit -> HH.HTML p i) -> HH.HTML p i
|
||||
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"]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}
|
||||
|
||||
module Application
|
||||
( getApplicationDev
|
||||
|
@ -15,7 +16,7 @@ module Application
|
|||
) where
|
||||
|
||||
import Control.Monad.Logger (liftLoc, runLoggingT)
|
||||
import Database.Persist.Sqlite (createSqlitePool, runSqlPool, sqlDatabase, sqlPoolSize)
|
||||
import Database.Persist.Sqlite (ConnectionPool, mkSqliteConnectionInfo, createSqlitePoolFromInfo, fkEnabled, runSqlPool, sqlDatabase, sqlPoolSize)
|
||||
import Import
|
||||
import Language.Haskell.TH.Syntax (qLocation)
|
||||
import Lens.Micro
|
||||
|
@ -28,12 +29,6 @@ import Network.Wai.Middleware.Gzip
|
|||
import Network.Wai.Middleware.MethodOverride
|
||||
import Network.Wai.Middleware.RequestLogger (Destination(Logger), IPAddrSource(..), OutputFormat(..), destination, mkRequestLogger, outputFormat)
|
||||
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.
|
||||
-- Don't forget to add new modules to your cabal file!
|
||||
|
@ -52,38 +47,37 @@ makeFoundation :: AppSettings -> IO App
|
|||
makeFoundation appSettings = do
|
||||
appHttpManager <- getGlobalManager
|
||||
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
|
||||
store <- EKG.newStore
|
||||
EKG.registerGcMetrics store
|
||||
appMetrics <- MM.initializeWith store
|
||||
appStatic <-
|
||||
(if appMutableStatic appSettings
|
||||
then staticDevel
|
||||
else static)
|
||||
(appStaticDir appSettings)
|
||||
let mkFoundation appConnPool = App { ..}
|
||||
let mkFoundation appConnPool = App {..}
|
||||
tempFoundation = mkFoundation (error "connPool forced in tempFoundation")
|
||||
logFunc = messageLoggerSource tempFoundation appLogger
|
||||
pool <-
|
||||
flip runLoggingT logFunc $
|
||||
createSqlitePool
|
||||
(sqlDatabase (appDatabaseConf appSettings))
|
||||
(sqlPoolSize (appDatabaseConf appSettings))
|
||||
runLoggingT
|
||||
(runSqlPool runMigrations pool)
|
||||
logFunc
|
||||
pool <- mkPool logFunc True
|
||||
poolMigrations <- mkPool logFunc False
|
||||
runLoggingT (runSqlPool runMigrations poolMigrations) logFunc
|
||||
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 foundation = do
|
||||
logWare <- makeLogWare foundation
|
||||
appPlain <- toWaiAppPlain foundation
|
||||
let store = appMetrics foundation ^. MM.metricsStore
|
||||
waiMetrics <- WM.registerWaiMetrics store
|
||||
return (logWare (makeMiddleware waiMetrics appPlain))
|
||||
return (logWare (makeMiddleware appPlain))
|
||||
|
||||
makeMiddleware :: WM.WaiMetrics -> Middleware
|
||||
makeMiddleware waiMetrics =
|
||||
WM.metrics waiMetrics .
|
||||
makeMiddleware :: Middleware
|
||||
makeMiddleware =
|
||||
acceptOverride .
|
||||
autohead .
|
||||
gzip def {gzipFiles = GzipPreCompressed GzipIgnore} .
|
||||
|
@ -127,7 +121,6 @@ getApplicationDev = do
|
|||
foundation <- makeFoundation settings
|
||||
wsettings <- getDevSettings (warpSettings foundation)
|
||||
app <- makeApplication foundation
|
||||
forkEKG foundation
|
||||
return (wsettings, app)
|
||||
|
||||
getAppSettings :: IO AppSettings
|
||||
|
@ -137,23 +130,12 @@ getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv
|
|||
develMain :: IO ()
|
||||
develMain = develMainHelper getApplicationDev
|
||||
|
||||
forkEKG :: App -> IO ()
|
||||
forkEKG foundation =
|
||||
let settings = appSettings foundation in
|
||||
for_ (appEkgHost settings) $ \ekgHost ->
|
||||
for_ (appEkgPort settings) $ \ekgPort ->
|
||||
EKG.forkServerWith
|
||||
(appMetrics foundation ^. MM.metricsStore)
|
||||
(encodeUtf8 ekgHost)
|
||||
ekgPort
|
||||
|
||||
-- | The @main@ function for an executable running this site.
|
||||
appMain :: IO ()
|
||||
appMain = do
|
||||
settings <- loadYamlSettingsArgs [configSettingsYmlValue] useEnv
|
||||
foundation <- makeFoundation settings
|
||||
app <- makeApplication foundation
|
||||
forkEKG foundation
|
||||
runSettings (warpSettings foundation) app
|
||||
|
||||
getApplicationRepl :: IO (Int, App, Application)
|
||||
|
|
|
@ -1,251 +1,265 @@
|
|||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||
|
||||
module Foundation where
|
||||
|
||||
import Import.NoFoundation
|
||||
import Database.Persist.Sql (ConnectionPool, runSqlPool)
|
||||
import Text.Hamlet (hamletFile)
|
||||
import Text.Jasmine (minifym)
|
||||
import PathPiece()
|
||||
|
||||
-- import Yesod.Auth.Dummy
|
||||
|
||||
import Yesod.Default.Util (addStaticContentExternal)
|
||||
import Yesod.Core.Types
|
||||
import Yesod.Auth.Message
|
||||
import qualified Network.Wai as NW
|
||||
import qualified Control.Monad.Metrics as MM
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import qualified Yesod.Core.Unsafe as Unsafe
|
||||
|
||||
data App = App
|
||||
{ appSettings :: AppSettings
|
||||
, appStatic :: Static -- ^ Settings for static file serving.
|
||||
, appConnPool :: ConnectionPool -- ^ Database connection pool.
|
||||
, appHttpManager :: Manager
|
||||
, appLogger :: Logger
|
||||
, appMetrics :: !MM.Metrics
|
||||
} deriving (Typeable)
|
||||
|
||||
mkYesodData "App" $(parseRoutesFile "config/routes")
|
||||
|
||||
deriving instance Typeable Route
|
||||
deriving instance Generic (Route App)
|
||||
|
||||
-- YesodPersist
|
||||
|
||||
instance YesodPersist App where
|
||||
type YesodPersistBackend App = SqlBackend
|
||||
runDB action = do
|
||||
master <- getYesod
|
||||
runSqlPool action (appConnPool master)
|
||||
|
||||
instance YesodPersistRunner App where
|
||||
getDBRunner = defaultGetDBRunner appConnPool
|
||||
|
||||
-- Yesod
|
||||
|
||||
instance Yesod App where
|
||||
approot = ApprootRequest $ \app req ->
|
||||
case appRoot (appSettings app) of
|
||||
Nothing -> getApprootText guessApproot app req
|
||||
Just root -> root
|
||||
|
||||
makeSessionBackend _ = Just <$> defaultClientSessionBackend
|
||||
10080 -- min (7 days)
|
||||
"config/client_session_key.aes"
|
||||
|
||||
yesodMiddleware = metricsMiddleware . defaultYesodMiddleware . defaultCsrfMiddleware
|
||||
|
||||
defaultLayout widget = do
|
||||
req <- getRequest
|
||||
master <- getYesod
|
||||
urlrender <- getUrlRender
|
||||
mmsg <- getMessage
|
||||
musername <- maybeAuthUsername
|
||||
muser <- (fmap.fmap) snd maybeAuthPair
|
||||
mcurrentRoute <- getCurrentRoute
|
||||
void $ mapM (incrementRouteEKG req) mcurrentRoute
|
||||
let msourceCodeUri = appSourceCodeUri (appSettings master)
|
||||
pc <- widgetToPageContent $ do
|
||||
setTitle "Espial"
|
||||
addAppScripts
|
||||
addStylesheet (StaticR css_tachyons_min_css)
|
||||
addStylesheet (StaticR css_main_css)
|
||||
$(widgetFile "default-layout")
|
||||
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
||||
|
||||
addStaticContent ext mime content = do
|
||||
master <- getYesod
|
||||
let staticDir = appStaticDir (appSettings master)
|
||||
addStaticContentExternal
|
||||
minifym
|
||||
genFileName
|
||||
staticDir
|
||||
(StaticR . flip StaticRoute [])
|
||||
ext
|
||||
mime
|
||||
content
|
||||
where
|
||||
genFileName lbs = "autogen-" ++ base64md5 lbs
|
||||
|
||||
shouldLogIO app _source level =
|
||||
pure $ appShouldLogAll (appSettings app) || level == LevelWarn || level == LevelError
|
||||
makeLogger = return . appLogger
|
||||
|
||||
authRoute _ = Just (AuthR LoginR)
|
||||
|
||||
isAuthorized (AuthR _) _ = pure Authorized
|
||||
isAuthorized _ _ = pure Authorized
|
||||
|
||||
defaultMessageWidget title body = do
|
||||
setTitle title
|
||||
toWidget [hamlet|
|
||||
<main .pv2.ph3.mh1>
|
||||
<div .w-100.mw8.center>
|
||||
<div .pa3.bg-near-white>
|
||||
<h1>#{title}
|
||||
^{body}
|
||||
|]
|
||||
|
||||
|
||||
isAuthenticated :: Handler AuthResult
|
||||
isAuthenticated = maybeAuthId >>= \case
|
||||
Just authId -> pure Authorized
|
||||
_ -> pure $ AuthenticationRequired
|
||||
|
||||
addAppScripts :: (MonadWidget m, HandlerSite m ~ App) => m ()
|
||||
addAppScripts = do
|
||||
addScript (StaticR js_app_min_js)
|
||||
|
||||
|
||||
-- popupLayout
|
||||
|
||||
popupLayout :: Widget -> Handler Html
|
||||
popupLayout widget = do
|
||||
req <- getRequest
|
||||
master <- getYesod
|
||||
mmsg <- getMessage
|
||||
musername <- maybeAuthUsername
|
||||
let msourceCodeUri = appSourceCodeUri (appSettings master)
|
||||
pc <- widgetToPageContent $ do
|
||||
addAppScripts
|
||||
addStylesheet (StaticR css_tachyons_min_css)
|
||||
addStylesheet (StaticR css_popup_css)
|
||||
$(widgetFile "popup-layout")
|
||||
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
||||
|
||||
|
||||
metricsMiddleware :: Handler a -> Handler a
|
||||
metricsMiddleware handler = do
|
||||
req <- getRequest
|
||||
mcurrentRoute <- getCurrentRoute
|
||||
void $ mapM (incrementRouteEKG req) mcurrentRoute
|
||||
handler
|
||||
|
||||
|
||||
incrementRouteEKG :: YesodRequest -> Route App -> Handler ()
|
||||
incrementRouteEKG req = MM.increment . (\r -> "route." <> r <> "." <> method) . pack . constrName
|
||||
where method = decodeUtf8 $ NW.requestMethod $ reqWaiRequest req
|
||||
|
||||
-- YesodAuth
|
||||
|
||||
instance YesodAuth App where
|
||||
type AuthId App = UserId
|
||||
-- authHttpManager = getHttpManager
|
||||
authPlugins _ = [dbAuthPlugin]
|
||||
authenticate = authenticateCreds
|
||||
loginDest = const HomeR
|
||||
logoutDest = const HomeR
|
||||
onLogin = maybeAuth >>= \case
|
||||
Nothing -> cpprint ("onLogin: could not find user" :: Text)
|
||||
Just (Entity _ uname) -> setSession userNameKey (userName uname)
|
||||
onLogout =
|
||||
deleteSession userNameKey
|
||||
redirectToReferer = const True
|
||||
|
||||
instance YesodAuthPersist App
|
||||
|
||||
instance MM.MonadMetrics Handler where
|
||||
getMetrics = pure . appMetrics =<< getYesod
|
||||
|
||||
-- session keys
|
||||
|
||||
maybeAuthUsername :: Handler (Maybe Text)
|
||||
maybeAuthUsername = do
|
||||
lookupSession userNameKey
|
||||
|
||||
ultDestKey :: Text
|
||||
ultDestKey = "_ULT"
|
||||
|
||||
userNameKey :: Text
|
||||
userNameKey = "_UNAME"
|
||||
|
||||
-- dbAuthPlugin
|
||||
|
||||
dbAuthPluginName :: Text
|
||||
dbAuthPluginName = "db"
|
||||
|
||||
dbAuthPlugin :: AuthPlugin App
|
||||
dbAuthPlugin = AuthPlugin dbAuthPluginName dbDispatch dbLoginHandler
|
||||
where
|
||||
dbDispatch "POST" ["login"] = dbPostLoginR >>= sendResponse
|
||||
dbDispatch _ _ = notFound
|
||||
dbLoginHandler toParent = do
|
||||
req <- getRequest
|
||||
lookupSession ultDestKey >>= \case
|
||||
Just dest | "logout" `isInfixOf` dest -> deleteSession ultDestKey
|
||||
_ -> pure ()
|
||||
setTitle "Espial | Log In"
|
||||
$(widgetFile "login")
|
||||
|
||||
dbLoginR :: AuthRoute
|
||||
dbLoginR = PluginR dbAuthPluginName ["login"]
|
||||
|
||||
dbPostLoginR :: AuthHandler master TypedContent
|
||||
dbPostLoginR = do
|
||||
mresult <- runInputPostResult (dbLoginCreds
|
||||
<$> ireq textField "username"
|
||||
<*> ireq textField "password")
|
||||
case mresult of
|
||||
FormSuccess creds -> setCredsRedirect creds
|
||||
_ -> loginErrorMessageI LoginR InvalidUsernamePass
|
||||
|
||||
dbLoginCreds :: Text -> Text -> Creds master
|
||||
dbLoginCreds username password =
|
||||
Creds
|
||||
{ credsPlugin = dbAuthPluginName
|
||||
, credsIdent = username
|
||||
, credsExtra = [("password", password)]
|
||||
}
|
||||
|
||||
authenticateCreds ::
|
||||
(MonadHandler m, HandlerSite m ~ App)
|
||||
=> Creds App
|
||||
-> m (AuthenticationResult App)
|
||||
authenticateCreds Creds {..} = do
|
||||
muser <-
|
||||
case credsPlugin of
|
||||
p | p == dbAuthPluginName -> liftHandler $ runDB $
|
||||
join <$> mapM (authenticatePassword credsIdent) (lookup "password" credsExtra)
|
||||
_ -> pure Nothing
|
||||
case muser of
|
||||
Nothing -> pure (UserError InvalidUsernamePass)
|
||||
Just (Entity uid _) -> pure (Authenticated uid)
|
||||
|
||||
-- Util
|
||||
|
||||
instance RenderMessage App FormMessage where
|
||||
renderMessage :: App -> [Lang] -> FormMessage -> Text
|
||||
renderMessage _ _ = defaultFormMessage
|
||||
|
||||
instance HasHttpManager App where
|
||||
getHttpManager :: App -> Manager
|
||||
getHttpManager = appHttpManager
|
||||
|
||||
unsafeHandler :: App -> Handler a -> IO a
|
||||
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
|
||||
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||
|
||||
module Foundation where
|
||||
|
||||
import Import.NoFoundation
|
||||
import Database.Persist.Sql (ConnectionPool, runSqlPool)
|
||||
import Text.Hamlet (hamletFile)
|
||||
import PathPiece()
|
||||
|
||||
import Yesod.Core.Types
|
||||
import Yesod.Auth.Message
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import qualified Yesod.Core.Unsafe as Unsafe
|
||||
import qualified Network.Wai as Wai
|
||||
|
||||
data App = App
|
||||
{ appSettings :: AppSettings
|
||||
, appStatic :: Static -- ^ Settings for static file serving.
|
||||
, appConnPool :: ConnectionPool -- ^ Database connection pool.
|
||||
, appHttpManager :: Manager
|
||||
, appLogger :: Logger
|
||||
} deriving (Typeable)
|
||||
|
||||
mkYesodData "App" $(parseRoutesFile "config/routes")
|
||||
|
||||
deriving instance Typeable Route
|
||||
deriving instance Generic (Route App)
|
||||
|
||||
-- YesodPersist
|
||||
|
||||
instance YesodPersist App where
|
||||
type YesodPersistBackend App = SqlBackend
|
||||
runDB action = do
|
||||
master <- getYesod
|
||||
runSqlPool action (appConnPool master)
|
||||
|
||||
instance YesodPersistRunner App where
|
||||
getDBRunner = defaultGetDBRunner appConnPool
|
||||
|
||||
session_timeout_minutes :: Int
|
||||
session_timeout_minutes = 10080 -- (7 days)
|
||||
|
||||
-- Yesod
|
||||
|
||||
instance Yesod App where
|
||||
approot = ApprootRequest \app req ->
|
||||
case appRoot (appSettings app) of
|
||||
Nothing -> getApprootText guessApproot app req
|
||||
Just root -> root
|
||||
|
||||
makeSessionBackend :: App -> IO (Maybe SessionBackend)
|
||||
makeSessionBackend App {appSettings} = do
|
||||
backend <-
|
||||
defaultClientSessionBackend
|
||||
session_timeout_minutes
|
||||
"config/client_session_key.aes"
|
||||
maybeSSLOnly $ pure (Just backend)
|
||||
where
|
||||
maybeSSLOnly =
|
||||
if appSSLOnly appSettings
|
||||
then sslOnlySessions
|
||||
else id
|
||||
|
||||
yesodMiddleware :: HandlerFor App res -> HandlerFor App res
|
||||
yesodMiddleware = customMiddleware . defaultYesodMiddleware . customCsrfMiddleware
|
||||
where
|
||||
customCsrfMiddleware handler = do
|
||||
maybeRoute <- getCurrentRoute
|
||||
dontCheckCsrf <- case maybeRoute of
|
||||
-- `maybeAuthId` checks for the validity of the Authorization
|
||||
-- header anyway, but it is still a good idea to limit this
|
||||
-- flexibility to designated routes.
|
||||
-- For the time being, `AddR` is the only route that accepts an
|
||||
-- authentication token.
|
||||
Just AddR -> isJust <$> lookupHeader "Authorization"
|
||||
_ -> pure False
|
||||
(if dontCheckCsrf then id else defaultCsrfMiddleware) handler
|
||||
|
||||
customMiddleware handler = do
|
||||
addHeader "X-Frame-Options" "DENY"
|
||||
yesod <- getYesod
|
||||
(if appSSLOnly (appSettings yesod)
|
||||
then sslOnlyMiddleware session_timeout_minutes
|
||||
else id) handler
|
||||
|
||||
defaultLayout widget = do
|
||||
req <- getRequest
|
||||
master <- getYesod
|
||||
urlrender <- getUrlRender
|
||||
mmsg <- getMessage
|
||||
musername <- maybeAuthUsername
|
||||
muser <- (fmap.fmap) snd maybeAuthPair
|
||||
let msourceCodeUri = appSourceCodeUri (appSettings master)
|
||||
pc <- widgetToPageContent do
|
||||
setTitle "Espial"
|
||||
addAppScripts
|
||||
addStylesheet (StaticR css_tachyons_min_css)
|
||||
addStylesheet (StaticR css_main_css)
|
||||
$(widgetFile "default-layout")
|
||||
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
||||
|
||||
shouldLogIO app _source level =
|
||||
pure $ appShouldLogAll (appSettings app) || level == LevelWarn || level == LevelError
|
||||
makeLogger = return . appLogger
|
||||
|
||||
authRoute _ = Just (AuthR LoginR)
|
||||
|
||||
isAuthorized (AuthR _) _ = pure Authorized
|
||||
isAuthorized _ _ = pure Authorized
|
||||
|
||||
defaultMessageWidget title body = do
|
||||
setTitle title
|
||||
toWidget [hamlet|
|
||||
<main .pv2.ph3.mh1>
|
||||
<div .w-100.mw8.center>
|
||||
<div .pa3.bg-near-white>
|
||||
<h1>#{title}
|
||||
^{body}
|
||||
|]
|
||||
|
||||
|
||||
isAuthenticated :: Handler AuthResult
|
||||
isAuthenticated = maybeAuthId >>= \case
|
||||
Just authId -> pure Authorized
|
||||
_ -> pure $ AuthenticationRequired
|
||||
|
||||
addAppScripts :: (MonadWidget m, HandlerSite m ~ App) => m ()
|
||||
addAppScripts = do
|
||||
addScript (StaticR js_app_min_js)
|
||||
|
||||
|
||||
-- popupLayout
|
||||
|
||||
popupLayout :: Widget -> Handler Html
|
||||
popupLayout widget = do
|
||||
req <- getRequest
|
||||
master <- getYesod
|
||||
mmsg <- getMessage
|
||||
musername <- maybeAuthUsername
|
||||
let msourceCodeUri = appSourceCodeUri (appSettings master)
|
||||
pc <- widgetToPageContent do
|
||||
addAppScripts
|
||||
addStylesheet (StaticR css_tachyons_min_css)
|
||||
addStylesheet (StaticR css_popup_css)
|
||||
$(widgetFile "popup-layout")
|
||||
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
||||
|
||||
|
||||
-- YesodAuth
|
||||
|
||||
instance YesodAuth App where
|
||||
type AuthId App = UserId
|
||||
authPlugins _ = [dbAuthPlugin]
|
||||
authenticate = authenticateCreds
|
||||
loginDest = const HomeR
|
||||
logoutDest = const HomeR
|
||||
onLogin = maybeAuth >>= \case
|
||||
Nothing -> cpprint ("onLogin: could not find user" :: Text)
|
||||
Just (Entity _ uname) -> setSession userNameKey (userName uname)
|
||||
onLogout =
|
||||
deleteSession userNameKey
|
||||
redirectToReferer = const True
|
||||
maybeAuthId = do
|
||||
req <- waiRequest
|
||||
let mAuthHeader = lookup "Authorization" (Wai.requestHeaders req)
|
||||
extractKey = stripPrefix "ApiKey " . TE.decodeUtf8
|
||||
case mAuthHeader of
|
||||
Just authHeader ->
|
||||
case extractKey authHeader of
|
||||
Just apiKey -> do
|
||||
user <- liftHandler $ runDB $ getApiKeyUser (ApiKey apiKey)
|
||||
let userId = entityKey <$> user
|
||||
pure userId
|
||||
-- Since we disable CSRF middleware in the presence of Authorization
|
||||
-- header, we need to explicitly check for the validity of the header
|
||||
-- content. Otherwise, a dummy Authorization header with garbage input
|
||||
-- could be provided to circumvent CSRF token requirement, making the app
|
||||
-- vulnerable to CSRF attacks.
|
||||
Nothing -> pure Nothing
|
||||
_ -> defaultMaybeAuthId
|
||||
|
||||
instance YesodAuthPersist App
|
||||
|
||||
-- session keys
|
||||
|
||||
maybeAuthUsername :: Handler (Maybe Text)
|
||||
maybeAuthUsername = do
|
||||
lookupSession userNameKey
|
||||
|
||||
ultDestKey :: Text
|
||||
ultDestKey = "_ULT"
|
||||
|
||||
userNameKey :: Text
|
||||
userNameKey = "_UNAME"
|
||||
|
||||
-- dbAuthPlugin
|
||||
|
||||
dbAuthPluginName :: Text
|
||||
dbAuthPluginName = "db"
|
||||
|
||||
dbAuthPlugin :: AuthPlugin App
|
||||
dbAuthPlugin = AuthPlugin dbAuthPluginName dbDispatch dbLoginHandler
|
||||
where
|
||||
dbDispatch :: Text -> [Text] -> AuthHandler App TypedContent
|
||||
dbDispatch "POST" ["login"] = dbPostLoginR >>= sendResponse
|
||||
dbDispatch _ _ = notFound
|
||||
dbLoginHandler toParent = do
|
||||
req <- getRequest
|
||||
lookupSession ultDestKey >>= \case
|
||||
Just dest | "logout" `isInfixOf` dest -> deleteSession ultDestKey
|
||||
_ -> pure ()
|
||||
setTitle "Espial | Log In"
|
||||
$(widgetFile "login")
|
||||
|
||||
dbLoginR :: AuthRoute
|
||||
dbLoginR = PluginR dbAuthPluginName ["login"]
|
||||
|
||||
dbPostLoginR :: AuthHandler master TypedContent
|
||||
dbPostLoginR = do
|
||||
mresult <- runInputPostResult (dbLoginCreds
|
||||
<$> ireq textField "username"
|
||||
<*> ireq textField "password")
|
||||
case mresult of
|
||||
FormSuccess creds -> setCredsRedirect creds
|
||||
_ -> loginErrorMessageI LoginR InvalidUsernamePass
|
||||
|
||||
dbLoginCreds :: Text -> Text -> Creds master
|
||||
dbLoginCreds username password =
|
||||
Creds
|
||||
{ credsPlugin = dbAuthPluginName
|
||||
, credsIdent = username
|
||||
, credsExtra = [("password", password)]
|
||||
}
|
||||
|
||||
authenticateCreds ::
|
||||
(MonadHandler m, HandlerSite m ~ App)
|
||||
=> Creds App
|
||||
-> m (AuthenticationResult App)
|
||||
authenticateCreds Creds {..} = do
|
||||
muser <-
|
||||
case credsPlugin of
|
||||
p | p == dbAuthPluginName -> liftHandler $ runDB $
|
||||
join <$> mapM (\pwd -> authenticatePassword credsIdent pwd) (lookup "password" credsExtra)
|
||||
_ -> pure Nothing
|
||||
case muser of
|
||||
Nothing -> pure (UserError InvalidUsernamePass)
|
||||
Just (Entity uid _) -> pure (Authenticated uid)
|
||||
|
||||
-- Util
|
||||
|
||||
instance RenderMessage App FormMessage where
|
||||
renderMessage :: App -> [Lang] -> FormMessage -> Text
|
||||
renderMessage _ _ = defaultFormMessage
|
||||
|
||||
instance HasHttpManager App where
|
||||
getHttpManager :: App -> Manager
|
||||
getHttpManager = appHttpManager
|
||||
|
||||
unsafeHandler :: App -> Handler a -> IO a
|
||||
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
|
||||
|
||||
|
|
|
@ -2,11 +2,12 @@ module Generic where
|
|||
|
||||
import GHC.Generics
|
||||
import ClassyPrelude.Yesod
|
||||
import Data.Kind (Type)
|
||||
|
||||
constrName :: (HasConstructor (Rep a), Generic a)=> a -> String
|
||||
constrName = genericConstrName . from
|
||||
|
||||
class HasConstructor (f :: * -> *) where
|
||||
class HasConstructor (f :: Type -> Type) where
|
||||
genericConstrName :: f x -> String
|
||||
|
||||
instance HasConstructor f => HasConstructor (D1 c f) where
|
||||
|
|
|
@ -8,14 +8,14 @@ getAccountSettingsR = do
|
|||
(_, user) <- requireAuthPair
|
||||
let accountSettingsEl = "accountSettings" :: Text
|
||||
let accountSettings = toAccountSettingsForm user
|
||||
defaultLayout $ do
|
||||
defaultLayout do
|
||||
$(widgetFile "user-settings")
|
||||
toWidgetBody [julius|
|
||||
app.userR = "@{UserR (UserNameP $ userName user)}";
|
||||
app.dat.accountSettings = #{ toJSON accountSettings } || [];
|
||||
|]
|
||||
toWidget [julius|
|
||||
PS['Main'].renderAccountSettings('##{rawJS accountSettingsEl}')(app.dat.accountSettings)();
|
||||
PS.renderAccountSettings('##{rawJS accountSettingsEl}')(app.dat.accountSettings)();
|
||||
|]
|
||||
|
||||
postEditAccountSettingsR :: Handler ()
|
||||
|
@ -34,17 +34,23 @@ getChangePasswordR = do
|
|||
|
||||
postChangePasswordR :: Handler Html
|
||||
postChangePasswordR = do
|
||||
userId <- requireAuthId
|
||||
mauthuname <- maybeAuthUsername
|
||||
mresult <- runInputPostResult ((,) <$> ireq textField "oldpassword" <*> ireq textField "newpassword")
|
||||
case (mauthuname, mresult) of
|
||||
(Just uname, FormSuccess (old, new)) -> do
|
||||
muser <- runDB (authenticatePassword uname old)
|
||||
case muser of
|
||||
Just _ -> do
|
||||
new' <- liftIO (hashPassword new)
|
||||
void $ runDB (update userId [UserPasswordHash CP.=. new'])
|
||||
setMessage "Password Changed Successfully"
|
||||
_ -> setMessage "Incorrect Old Password"
|
||||
(userId, user) <- requireAuthPair
|
||||
runInputPostResult ((,) <$> ireq textField "oldpassword" <*> ireq textField "newpassword") >>= \case
|
||||
FormSuccess (old, new) -> do
|
||||
runDB (authenticatePassword (userName user) old) >>= \case
|
||||
Nothing -> setMessage "Incorrect Old Password"
|
||||
Just _ -> validateNewPassword new >>= \case
|
||||
Just newValid -> do
|
||||
newHash <- liftIO (hashPassword newValid)
|
||||
void $ runDB (update userId [UserPasswordHash CP.=. newHash])
|
||||
setMessage "Password Changed Successfully"
|
||||
_ -> pure ()
|
||||
_ -> setMessage "Missing Required Fields"
|
||||
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
|
||||
|
|
|
@ -1,67 +1,90 @@
|
|||
module Handler.Add where
|
||||
|
||||
import Import
|
||||
import Handler.Archive
|
||||
import Data.List (nub)
|
||||
|
||||
-- View
|
||||
|
||||
getAddViewR :: Handler Html
|
||||
getAddViewR = do
|
||||
userId <- requireAuthId
|
||||
|
||||
murl <- lookupGetParam "url"
|
||||
mformdb <- runDB (pure . fmap _toBookmarkForm =<< fetchBookmarkByUrl userId murl)
|
||||
formurl <- bookmarkFormUrl
|
||||
|
||||
let renderEl = "addForm" :: Text
|
||||
|
||||
popupLayout $ do
|
||||
toWidget [whamlet|
|
||||
<div id="#{ renderEl }">
|
||||
|]
|
||||
toWidgetBody [julius|
|
||||
app.dat.bmark = #{ toJSON (fromMaybe formurl mformdb) };
|
||||
|]
|
||||
toWidget [julius|
|
||||
PS['Main'].renderAddForm('##{rawJS renderEl}')(app.dat.bmark)();
|
||||
|]
|
||||
|
||||
bookmarkFormUrl :: Handler BookmarkForm
|
||||
bookmarkFormUrl = do
|
||||
Entity _ user <- requireAuth
|
||||
BookmarkForm
|
||||
<$> (lookupGetParam "url" >>= pure . fromMaybe "")
|
||||
<*> (lookupGetParam "title")
|
||||
<*> (lookupGetParam "description" >>= pure . fmap Textarea)
|
||||
<*> (lookupGetParam "tags")
|
||||
<*> (lookupGetParam "private" >>= pure . fmap parseChk <&> (<|> Just (userPrivateDefault user)))
|
||||
<*> (lookupGetParam "toread" >>= pure . fmap parseChk)
|
||||
<*> pure Nothing
|
||||
<*> pure Nothing
|
||||
<*> pure Nothing
|
||||
<*> pure Nothing
|
||||
<*> pure Nothing
|
||||
where
|
||||
parseChk s = s == "yes" || s == "on"
|
||||
|
||||
-- API
|
||||
|
||||
postAddR :: Handler ()
|
||||
postAddR = do
|
||||
bookmarkForm <- requireCheckJsonBody
|
||||
_handleFormSuccess bookmarkForm >>= \case
|
||||
(Created, bid) -> sendStatusJSON created201 bid
|
||||
(Updated, _) -> sendResponseStatus noContent204 ()
|
||||
|
||||
_handleFormSuccess :: BookmarkForm -> Handler (UpsertResult, Key Bookmark)
|
||||
_handleFormSuccess bookmarkForm = do
|
||||
(userId, user) <- requireAuthPair
|
||||
bm <- liftIO $ _toBookmark userId bookmarkForm
|
||||
(res, kbid) <- runDB (upsertBookmark userId mkbid bm tags)
|
||||
whenM (shouldArchiveBookmark user kbid) $
|
||||
void $ async (archiveBookmarkUrl kbid (unpack (bookmarkHref bm)))
|
||||
pure (res, kbid)
|
||||
where
|
||||
mkbid = BookmarkKey <$> _bid bookmarkForm
|
||||
tags = maybe [] (nub . words) (_tags bookmarkForm)
|
||||
module Handler.Add where
|
||||
|
||||
import Import
|
||||
import Handler.Archive
|
||||
import Data.List (nub)
|
||||
import qualified Data.Text as T (replace)
|
||||
|
||||
-- View
|
||||
|
||||
getAddViewR :: Handler Html
|
||||
getAddViewR = do
|
||||
userId <- requireAuthId
|
||||
|
||||
murl <- lookupGetParam "url"
|
||||
mBookmarkDb <- runDB (fetchBookmarkByUrl userId murl)
|
||||
let mformdb = fmap _toBookmarkForm mBookmarkDb
|
||||
formurl <- bookmarkFormUrl
|
||||
|
||||
let renderEl = "addForm" :: Text
|
||||
|
||||
popupLayout do
|
||||
toWidget [whamlet|
|
||||
<div id="#{ renderEl }">
|
||||
|]
|
||||
toWidgetBody [julius|
|
||||
app.dat.bmark = #{ toJSON (fromMaybe formurl mformdb) };
|
||||
|]
|
||||
toWidget [julius|
|
||||
PS.renderAddForm('##{rawJS renderEl}')(app.dat.bmark)();
|
||||
|]
|
||||
|
||||
bookmarkFormUrl :: Handler BookmarkForm
|
||||
bookmarkFormUrl = do
|
||||
Entity _ user <- requireAuth
|
||||
url <- lookupGetParam "url" <&> fromMaybe ""
|
||||
title <- lookupGetParam "title"
|
||||
description <- lookupGetParam "description" <&> fmap Textarea
|
||||
tags <- lookupGetParam "tags"
|
||||
private <- lookupGetParam "private" <&> fmap parseChk <&> (<|> Just (userPrivateDefault user))
|
||||
toread <- lookupGetParam "toread" <&> fmap parseChk
|
||||
pure $
|
||||
BookmarkForm
|
||||
{ _url = url
|
||||
, _title = title
|
||||
, _description = description
|
||||
, _tags = tags
|
||||
, _private = private
|
||||
, _toread = toread
|
||||
, _bid = Nothing
|
||||
, _slug = Nothing
|
||||
, _selected = Nothing
|
||||
, _time = Nothing
|
||||
, _archiveUrl = Nothing
|
||||
}
|
||||
where
|
||||
parseChk s = s == "yes" || s == "on" || s == "true" || s == "1"
|
||||
|
||||
-- API
|
||||
|
||||
postAddR :: Handler Text
|
||||
postAddR = do
|
||||
bookmarkForm <- requireCheckJsonBody
|
||||
_handleFormSuccess bookmarkForm >>= \case
|
||||
Created bid -> sendStatusJSON created201 bid
|
||||
Updated _ -> sendResponseStatus noContent204 ()
|
||||
Failed s -> sendResponseStatus status400 s
|
||||
|
||||
_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
|
||||
|
|
|
@ -2,7 +2,9 @@ module Handler.Archive where
|
|||
|
||||
import Import
|
||||
import Data.Function ((&))
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as AP
|
||||
import Data.Char (ord)
|
||||
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.Lazy as LBS
|
||||
import qualified Data.ByteString.Char8 as BS8
|
||||
|
@ -10,45 +12,56 @@ import qualified Network.HTTP.Client as NH
|
|||
import qualified Network.HTTP.Client.TLS as NH
|
||||
import qualified Network.HTTP.Types.Status as NH
|
||||
import qualified Web.FormUrlEncoded as WH
|
||||
import qualified Control.Monad.Metrics as MM
|
||||
import HTMLEntities.Decoder (htmlEncodedText)
|
||||
import Data.Text.Lazy.Builder (toLazyText)
|
||||
import Network.Wai (requestHeaderHost)
|
||||
import qualified Network.Connection as NC
|
||||
|
||||
shouldArchiveBookmark :: User -> Key Bookmark -> Handler Bool
|
||||
shouldArchiveBookmark user kbid = do
|
||||
shouldArchiveBookmark user kbid =
|
||||
runDB (get kbid) >>= \case
|
||||
Nothing -> pure False
|
||||
Just bm -> do
|
||||
pure $
|
||||
(isNothing $ bookmarkArchiveHref bm) &&
|
||||
(bookmarkShared bm)
|
||||
&& not (_isArchiveBlacklisted bm)
|
||||
&& not (userPrivacyLock user)
|
||||
&& userArchiveDefault user
|
||||
Nothing -> pure False
|
||||
|
||||
Just bm -> do
|
||||
pure $
|
||||
isNothing (bookmarkArchiveHref bm) &&
|
||||
bookmarkShared bm
|
||||
&& not (_isArchiveBlacklisted bm)
|
||||
&& 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 kbid url =
|
||||
(_fetchArchiveSubmitInfo >>= \case
|
||||
Left e -> do
|
||||
MM.increment "archive.fetchSubmitId_noparse"
|
||||
$(logError) (pack e)
|
||||
Right submitInfo -> do
|
||||
userId <- requireAuthId
|
||||
let req = _buildArchiveSubmitRequest submitInfo url
|
||||
MM.increment "archive.submit"
|
||||
res <- liftIO $ NH.httpLbs req =<< NH.getGlobalManager
|
||||
req <- _buildArchiveSubmitRequest submitInfo url
|
||||
manager <- getArchiveManager
|
||||
res <- liftIO $ NH.httpLbs req manager
|
||||
let status = NH.responseStatus res
|
||||
MM.increment ("archive.submit_status_" <> (pack.show) (NH.statusCode status))
|
||||
let updateArchiveUrl = runDB . updateBookmarkArchiveUrl userId kbid . Just
|
||||
let updateArchiveUrl url' = runDB $ updateBookmarkArchiveUrl userId kbid $ Just url'
|
||||
headers = NH.responseHeaders res
|
||||
case status of
|
||||
s | s == NH.status200 ->
|
||||
for_ (lookup "Refresh" headers >>= _parseRefreshHeaderUrl) updateArchiveUrl
|
||||
s | s == NH.status302 ->
|
||||
s | s == NH.status302 || s == NH.status307 ->
|
||||
for_ (lookup "Location" headers) (updateArchiveUrl . decodeUtf8)
|
||||
_ -> $(logError) (pack (show res)))
|
||||
`catch` (\(e::SomeException) -> ($(logError) $ (pack.show) e) >> throwIO e)
|
||||
|
||||
_isArchiveBlacklisted :: Bookmark -> Bool
|
||||
_isArchiveBlacklisted (Bookmark {..}) =
|
||||
_isArchiveBlacklisted Bookmark {..} =
|
||||
[ "hulu"
|
||||
, "livestream"
|
||||
, "netflix"
|
||||
|
@ -64,43 +77,73 @@ _isArchiveBlacklisted (Bookmark {..}) =
|
|||
_parseRefreshHeaderUrl :: ByteString -> Maybe Text
|
||||
_parseRefreshHeaderUrl h = do
|
||||
let u = BS8.drop 1 $ BS8.dropWhile (/= '=') h
|
||||
if (not (null u))
|
||||
if not (null u)
|
||||
then Just $ decodeUtf8 u
|
||||
else Nothing
|
||||
|
||||
_buildArchiveSubmitRequest :: (String, String) -> String -> NH.Request
|
||||
_buildArchiveSubmitRequest (action, submitId) href =
|
||||
NH.parseRequest_ ("POST " <> action) & \r ->
|
||||
r { NH.requestHeaders =
|
||||
[ ("User-Agent", _archiveUserAgent)
|
||||
, ("Content-Type", "application/x-www-form-urlencoded")
|
||||
]
|
||||
, NH.requestBody = NH.RequestBodyLBS $ WH.urlEncodeAsForm ((
|
||||
[ ("submitid" , submitId)
|
||||
, ("url", href)
|
||||
]) :: [(String, String)])
|
||||
, NH.redirectCount = 0
|
||||
}
|
||||
|
||||
_fetchArchiveSubmitInfo :: Handler (Either String (String , String))
|
||||
_fetchArchiveSubmitInfo = do
|
||||
MM.increment "archive.fetchSubmitId"
|
||||
res <- liftIO $ NH.httpLbs buildSubmitRequest =<< NH.getGlobalManager
|
||||
MM.increment ("archive.fetchSubmitId_status_" <> (pack.show) (NH.statusCode (NH.responseStatus res)))
|
||||
req <- buildRequest "https://archive.li/"
|
||||
manager <- getArchiveManager
|
||||
res <- liftIO $ NH.httpLbs req manager
|
||||
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)]}
|
||||
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)
|
||||
|
||||
_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)
|
||||
_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 *> pure () <|> AP.anyChar *> go
|
||||
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 =
|
||||
[ ("Cache-Control", "max-age=0")
|
||||
, ("User-Agent", ua)
|
||||
]
|
||||
}
|
||||
|
||||
_archiveUserAgent :: Handler ByteString
|
||||
_archiveUserAgent = do
|
||||
mHost <- requestHeaderHost . reqWaiRequest <$> getRequest
|
||||
pure $ "espial-" <> maybe "" (BS8.takeWhile (/= ':')) mHost
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import Import
|
|||
|
||||
import Data.FileEmbed (embedFile)
|
||||
import Text.Read
|
||||
import Data.Aeson as A
|
||||
|
||||
-- These handlers embed files in the executable at compile time to avoid a
|
||||
-- runtime dependency, and for efficiency.
|
||||
|
@ -21,11 +22,49 @@ getRobotsR = return $ TypedContent typePlain
|
|||
|
||||
|
||||
lookupPagingParams :: Handler (Maybe Int64, Maybe Int64)
|
||||
lookupPagingParams = do
|
||||
cq <- fmap parseMaybe (lookupGetParam "count")
|
||||
cs <- fmap parseMaybe (lookupSession "count")
|
||||
for_ cq (setSession "count" . (pack . show))
|
||||
pq <- fmap parseMaybe (lookupGetParam "page")
|
||||
pure (cq <|> cs, pq)
|
||||
lookupPagingParams =
|
||||
(,)
|
||||
<$> getUrlSessionParam "count"
|
||||
<*> getUrlParam "page"
|
||||
|
||||
getUrlParam :: (Read a) => Text -> Handler (Maybe a)
|
||||
getUrlParam name = do
|
||||
fmap parseMaybe (lookupGetParam name)
|
||||
where
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -5,5 +5,5 @@ module Handler.Docs where
|
|||
import Import
|
||||
|
||||
getDocsSearchR :: Handler Html
|
||||
getDocsSearchR = popupLayout $
|
||||
getDocsSearchR = popupLayout
|
||||
$(widgetFile "docs-search")
|
||||
|
|
|
@ -11,16 +11,16 @@ import Import
|
|||
deleteDeleteR :: Int64 -> Handler Html
|
||||
deleteDeleteR bid = do
|
||||
userId <- requireAuthId
|
||||
runDB $ do
|
||||
runDB do
|
||||
let k_bid = BookmarkKey bid
|
||||
_ <- requireResource userId k_bid
|
||||
deleteCascade k_bid
|
||||
delete k_bid
|
||||
return ""
|
||||
|
||||
postReadR :: Int64 -> Handler Html
|
||||
postReadR bid = do
|
||||
userId <- requireAuthId
|
||||
runDB $ do
|
||||
runDB do
|
||||
let k_bid = BookmarkKey bid
|
||||
_ <- requireResource userId k_bid
|
||||
update k_bid [BookmarkToRead =. False]
|
||||
|
@ -37,7 +37,7 @@ postUnstarR bid = _setSelected bid False
|
|||
_setSelected :: Int64 -> Bool -> Handler Html
|
||||
_setSelected bid selected = do
|
||||
userId <- requireAuthId
|
||||
runDB $ do
|
||||
runDB do
|
||||
let k_bid = BookmarkKey bid
|
||||
bm <- requireResource userId k_bid
|
||||
update k_bid [BookmarkSelected =. selected]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
module Handler.Notes where
|
||||
|
||||
import Import
|
||||
|
@ -6,8 +7,8 @@ import Handler.Common (lookupPagingParams)
|
|||
import qualified Data.Aeson as A
|
||||
import qualified Data.Text as T
|
||||
import Yesod.RssFeed
|
||||
import Text.Blaze.Html (toHtml)
|
||||
import qualified Text.Blaze.Html5 as H
|
||||
import qualified Network.Wai.Internal as W
|
||||
|
||||
getNotesR :: UserNameP -> Handler Html
|
||||
getNotesR unamep@(UserNameP uname) = do
|
||||
|
@ -17,9 +18,9 @@ getNotesR unamep@(UserNameP uname) = do
|
|||
mquery <- lookupGetParam queryp
|
||||
let limit = maybe 20 fromIntegral limit'
|
||||
page = maybe 1 fromIntegral page'
|
||||
mqueryp = fmap (\q -> (queryp, q)) mquery
|
||||
isowner = maybe False (== uname) mauthuname
|
||||
(bcount, notes) <- runDB $ do
|
||||
mqueryp = fmap (queryp,) mquery
|
||||
isowner = Just uname == mauthuname
|
||||
(bcount, notes) <- runDB do
|
||||
Entity userId user <- getBy404 (UniqueUserName uname)
|
||||
let sharedp = if isowner then SharedAll else SharedPublic
|
||||
when (not isowner && userPrivacyLock user)
|
||||
|
@ -27,7 +28,7 @@ getNotesR unamep@(UserNameP uname) = do
|
|||
getNoteList userId mquery sharedp limit page
|
||||
req <- getRequest
|
||||
mroute <- getCurrentRoute
|
||||
defaultLayout $ do
|
||||
defaultLayout do
|
||||
rssLink (NotesFeedR unamep) "feed"
|
||||
let pager = $(widgetFile "pager")
|
||||
search = $(widgetFile "search")
|
||||
|
@ -39,14 +40,14 @@ getNotesR unamep@(UserNameP uname) = do
|
|||
app.dat.isowner = #{ isowner };
|
||||
|]
|
||||
toWidget [julius|
|
||||
PS['Main'].renderNotes('##{rawJS renderEl}')(app.dat.notes)();
|
||||
PS.renderNotes('##{rawJS renderEl}')(app.dat.notes)();
|
||||
|]
|
||||
|
||||
getNoteR :: UserNameP -> NtSlug -> Handler Html
|
||||
getNoteR unamep@(UserNameP uname) slug = do
|
||||
mauthuname <- maybeAuthUsername
|
||||
let renderEl = "note" :: Text
|
||||
isowner = maybe False (== uname) mauthuname
|
||||
isowner = Just uname == mauthuname
|
||||
note <-
|
||||
runDB $
|
||||
do Entity userId user <- getBy404 (UniqueUserName uname)
|
||||
|
@ -55,7 +56,7 @@ getNoteR unamep@(UserNameP uname) slug = do
|
|||
when (not isowner && (userPrivacyLock user || (not . noteShared . entityVal) note))
|
||||
(redirect (AuthR LoginR))
|
||||
pure note
|
||||
defaultLayout $ do
|
||||
defaultLayout do
|
||||
$(widgetFile "note")
|
||||
toWidgetBody [julius|
|
||||
app.userR = "@{UserR unamep}";
|
||||
|
@ -63,40 +64,47 @@ getNoteR unamep@(UserNameP uname) slug = do
|
|||
app.dat.isowner = #{ isowner };
|
||||
|]
|
||||
toWidget [julius|
|
||||
PS['Main'].renderNote('##{rawJS renderEl}')(app.dat.note)();
|
||||
PS.renderNote('##{rawJS renderEl}')(app.dat.note)();
|
||||
|]
|
||||
|
||||
getAddNoteSlimViewR :: Handler Html
|
||||
getAddNoteSlimViewR = do
|
||||
Entity userId user <- requireAuth
|
||||
getAddNoteViewR (UserNameP (userName user))
|
||||
|
||||
getAddNoteViewR :: UserNameP -> Handler Html
|
||||
getAddNoteViewR unamep@(UserNameP uname) = do
|
||||
userId <- requireAuthId
|
||||
note <- liftIO . _toNote userId =<< noteFormUrl
|
||||
let renderEl = "note" :: Text
|
||||
note <- liftIO $ Entity (NoteKey 0) <$> _toNote userId (NoteForm Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing)
|
||||
defaultLayout $ do
|
||||
enote = Entity (NoteKey 0) note
|
||||
defaultLayout do
|
||||
$(widgetFile "note")
|
||||
toWidgetBody [julius|
|
||||
app.userR = "@{UserR unamep}";
|
||||
app.noteR = "@{NoteR unamep (noteSlug (entityVal note))}";
|
||||
app.dat.note = #{ toJSON note } || [];
|
||||
app.noteR = "@{NoteR unamep (noteSlug (entityVal enote))}";
|
||||
app.dat.note = #{ toJSON enote } || [];
|
||||
|]
|
||||
toWidget [julius|
|
||||
PS['Main'].renderNote('##{rawJS renderEl}')(app.dat.note)();
|
||||
PS.renderNote('##{rawJS renderEl}')(app.dat.note)();
|
||||
|]
|
||||
|
||||
deleteDeleteNoteR :: Int64 -> Handler Html
|
||||
deleteDeleteNoteR nid = do
|
||||
userId <- requireAuthId
|
||||
runDB $ do
|
||||
runDB do
|
||||
let k_nid = NoteKey nid
|
||||
_ <- requireResource userId k_nid
|
||||
deleteCascade k_nid
|
||||
delete k_nid
|
||||
return ""
|
||||
|
||||
postAddNoteR :: Handler ()
|
||||
postAddNoteR :: Handler Text
|
||||
postAddNoteR = do
|
||||
noteForm <- requireCheckJsonBody
|
||||
_handleFormSuccess noteForm >>= \case
|
||||
(Created, nid) -> sendStatusJSON created201 nid
|
||||
(Updated, _) -> sendResponseStatus noContent204 ()
|
||||
Created nid -> sendStatusJSON created201 nid
|
||||
Updated _ -> sendResponseStatus noContent204 ()
|
||||
Failed s -> sendResponseStatus status400 s
|
||||
|
||||
requireResource :: UserId -> Key Note -> DBM Handler Note
|
||||
requireResource userId k_nid = do
|
||||
|
@ -105,7 +113,7 @@ requireResource userId k_nid = do
|
|||
then return nnote
|
||||
else notFound
|
||||
|
||||
_handleFormSuccess :: NoteForm -> Handler (UpsertResult, Key Note)
|
||||
_handleFormSuccess :: NoteForm -> Handler (UpsertResult (Key Note))
|
||||
_handleFormSuccess noteForm = do
|
||||
userId <- requireAuthId
|
||||
note <- liftIO $ _toNote userId noteForm
|
||||
|
@ -130,30 +138,51 @@ instance ToJSON NoteForm where toJSON = A.genericToJSON gNoteFormOptions
|
|||
gNoteFormOptions :: A.Options
|
||||
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 {..} = do
|
||||
time <- liftIO getCurrentTime
|
||||
slug <- maybe mkNtSlug pure _slug
|
||||
pure $
|
||||
Note
|
||||
userId
|
||||
slug
|
||||
(length _text)
|
||||
(fromMaybe "" _title)
|
||||
(maybe "" unTextarea _text)
|
||||
(fromMaybe False _isMarkdown)
|
||||
(fromMaybe False _shared)
|
||||
(fromMaybe time (fmap unUTCTimeStr _created))
|
||||
(fromMaybe time (fmap unUTCTimeStr _updated))
|
||||
{ noteUserId = userId
|
||||
, noteSlug = slug
|
||||
, noteLength = length _text
|
||||
, noteTitle = fromMaybe "" _title
|
||||
, noteText = maybe "" unTextarea _text
|
||||
, noteIsMarkdown = Just True == _isMarkdown
|
||||
, noteShared = Just True == _shared
|
||||
, noteCreated = maybe time unUTCTimeStr _created
|
||||
, noteUpdated = maybe time unUTCTimeStr _updated
|
||||
}
|
||||
|
||||
noteToRssEntry :: UserNameP -> Entity Note -> FeedEntry (Route App)
|
||||
noteToRssEntry usernamep (Entity entryId entry) =
|
||||
FeedEntry { feedEntryLink = NoteR usernamep (noteSlug entry)
|
||||
, feedEntryUpdated = (noteUpdated entry)
|
||||
, feedEntryTitle = (noteTitle entry)
|
||||
, feedEntryContent = (toHtml (noteText entry))
|
||||
, feedEntryEnclosure = Nothing
|
||||
}
|
||||
noteToRssEntry :: (Route App -> Text) -> UserNameP -> Entity Note -> FeedEntry Text
|
||||
noteToRssEntry render usernamep (Entity entryId entry) =
|
||||
FeedEntry
|
||||
{ feedEntryLink = render $ NoteR usernamep (noteSlug entry)
|
||||
, feedEntryUpdated = noteUpdated entry
|
||||
, feedEntryTitle = noteTitle entry
|
||||
, feedEntryContent = toHtml (noteText entry)
|
||||
, feedEntryEnclosure = Nothing
|
||||
, feedEntryCategories = []
|
||||
}
|
||||
|
||||
getNotesFeedR :: UserNameP -> Handler RepRss
|
||||
getNotesFeedR unamep@(UserNameP uname) = do
|
||||
|
@ -162,23 +191,37 @@ getNotesFeedR unamep@(UserNameP uname) = do
|
|||
mquery <- lookupGetParam "query"
|
||||
let limit = maybe 20 fromIntegral limit'
|
||||
page = maybe 1 fromIntegral page'
|
||||
isowner = maybe False (== uname) mauthuname
|
||||
(_, notes) <- runDB $ do
|
||||
isowner = Just uname == mauthuname
|
||||
sharedp = if isowner then SharedAll else SharedPublic
|
||||
(_, notes) <- runDB do
|
||||
Entity userId user <- getBy404 (UniqueUserName uname)
|
||||
when (not isowner && userPrivacyLock user)
|
||||
(redirect (AuthR LoginR))
|
||||
getNoteList userId mquery SharedPublic limit page
|
||||
getNoteList userId mquery sharedp limit page
|
||||
render <- getUrlRender
|
||||
let (descr :: Html) = toHtml $ H.text (uname <> " notes")
|
||||
entries = map (noteToRssEntry unamep) notes
|
||||
entries = map (noteToRssEntry render unamep) notes
|
||||
updated <- case maximumMay (map feedEntryUpdated entries) of
|
||||
Nothing -> liftIO $ getCurrentTime
|
||||
Nothing -> liftIO getCurrentTime
|
||||
Just m -> return m
|
||||
rssFeed $ Feed (uname <> " notes")
|
||||
(NotesFeedR unamep)
|
||||
(NotesR unamep)
|
||||
uname
|
||||
descr
|
||||
"en"
|
||||
updated
|
||||
Nothing
|
||||
entries
|
||||
(feedLinkSelf, feedLinkHome) <- getFeedLinkSelf
|
||||
rssFeedText $
|
||||
Feed
|
||||
{ feedTitle = uname <> " notes"
|
||||
, 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)
|
||||
|
|
|
@ -1,106 +1,172 @@
|
|||
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||
module Handler.User where
|
||||
|
||||
import qualified Data.Text as T
|
||||
import Handler.Common (lookupPagingParams)
|
||||
import Import
|
||||
import Text.Blaze.Html (toHtml)
|
||||
import qualified Text.Blaze.Html5 as H
|
||||
import Yesod.RssFeed
|
||||
|
||||
getUserR :: UserNameP -> Handler Html
|
||||
getUserR uname@(UserNameP name) = do
|
||||
_getUser uname SharedAll FilterAll (TagsP [])
|
||||
|
||||
getUserSharedR :: UserNameP -> SharedP -> Handler Html
|
||||
getUserSharedR uname sharedp =
|
||||
_getUser uname sharedp FilterAll (TagsP [])
|
||||
|
||||
getUserFilterR :: UserNameP -> FilterP -> Handler Html
|
||||
getUserFilterR uname filterp =
|
||||
_getUser uname SharedAll filterp (TagsP [])
|
||||
|
||||
getUserTagsR :: UserNameP -> TagsP -> Handler Html
|
||||
getUserTagsR uname pathtags =
|
||||
_getUser uname SharedAll FilterAll pathtags
|
||||
|
||||
_getUser :: UserNameP -> SharedP -> FilterP -> TagsP -> Handler Html
|
||||
_getUser unamep@(UserNameP uname) sharedp' filterp' (TagsP pathtags) = do
|
||||
mauthuname <- maybeAuthUsername
|
||||
(limit', page') <- lookupPagingParams
|
||||
let limit = maybe 120 fromIntegral limit'
|
||||
page = maybe 1 fromIntegral page'
|
||||
isowner = maybe False (== uname) mauthuname
|
||||
sharedp = if isowner then sharedp' else SharedPublic
|
||||
filterp = case filterp' of
|
||||
FilterSingle _ -> filterp'
|
||||
_ -> if isowner then filterp' else FilterAll
|
||||
isAll = filterp == FilterAll && sharedp == SharedAll && pathtags == []
|
||||
queryp = "query" :: Text
|
||||
mquery <- lookupGetParam queryp
|
||||
let mqueryp = fmap (\q -> (queryp, q)) mquery
|
||||
(bcount, bmarks, alltags) <-
|
||||
runDB $
|
||||
do Entity userId user <- getBy404 (UniqueUserName uname)
|
||||
when (not isowner && userPrivacyLock user)
|
||||
(redirect (AuthR LoginR))
|
||||
(cnt, bm) <- bookmarksQuery userId sharedp filterp pathtags mquery limit page
|
||||
tg <- tagsQuery bm
|
||||
pure (cnt, bm, tg)
|
||||
when (bcount == 0) (case filterp of FilterSingle _ -> notFound; _ -> pure ())
|
||||
mroute <- getCurrentRoute
|
||||
req <- getRequest
|
||||
defaultLayout $ do
|
||||
let pager = $(widgetFile "pager")
|
||||
search = $(widgetFile "search")
|
||||
renderEl = "bookmarks" :: Text
|
||||
rssLink (UserFeedR unamep) "feed"
|
||||
$(widgetFile "user")
|
||||
toWidgetBody [julius|
|
||||
app.dat.bmarks = #{ toJSON $ toBookmarkFormList bmarks alltags } || [];
|
||||
app.dat.isowner = #{ isowner };
|
||||
app.userR = "@{UserR unamep}";
|
||||
|]
|
||||
toWidget [julius|
|
||||
PS['Main'].renderBookmarks('##{rawJS renderEl}')(app.dat.bmarks)();
|
||||
|]
|
||||
|
||||
bookmarkToRssEntry :: Entity Bookmark -> FeedEntry Text
|
||||
bookmarkToRssEntry (Entity entryId entry) =
|
||||
FeedEntry { feedEntryLink = (bookmarkHref entry)
|
||||
, feedEntryUpdated = (bookmarkTime entry)
|
||||
, feedEntryTitle = (bookmarkDescription entry)
|
||||
, feedEntryContent = (toHtml (bookmarkExtended entry))
|
||||
, feedEntryEnclosure = Nothing
|
||||
}
|
||||
|
||||
getUserFeedR :: UserNameP -> Handler RepRss
|
||||
getUserFeedR unamep@(UserNameP uname) = do
|
||||
mauthuname <- maybeAuthUsername
|
||||
(limit', page') <- lookupPagingParams
|
||||
let limit = maybe 120 fromIntegral limit'
|
||||
page = maybe 1 fromIntegral page'
|
||||
queryp = "query" :: Text
|
||||
isowner = maybe False (== uname) mauthuname
|
||||
mquery <- lookupGetParam queryp
|
||||
(_, bmarks) <-
|
||||
runDB $
|
||||
do Entity userId user <- getBy404 (UniqueUserName uname)
|
||||
when (not isowner && userPrivacyLock user)
|
||||
(redirect (AuthR LoginR))
|
||||
bookmarksQuery userId SharedPublic FilterAll [] mquery limit page
|
||||
let (descr :: Html) = toHtml $ H.text ("Bookmarks saved by " <> uname)
|
||||
entries = map bookmarkToRssEntry bmarks
|
||||
updated <- case maximumMay (map feedEntryUpdated entries) of
|
||||
Nothing -> liftIO $ getCurrentTime
|
||||
Just m -> return m
|
||||
render <- getUrlRender
|
||||
rssFeedText $ Feed ("espial " <> uname)
|
||||
(render (UserFeedR unamep))
|
||||
(render (UserR unamep))
|
||||
uname
|
||||
descr
|
||||
"en"
|
||||
updated
|
||||
Nothing
|
||||
entries
|
||||
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
module Handler.User where
|
||||
|
||||
import qualified Data.Text as T
|
||||
import Handler.Common
|
||||
import Import
|
||||
import qualified Text.Blaze.Html5 as H
|
||||
import Yesod.RssFeed
|
||||
import qualified Data.Map as Map
|
||||
import qualified Network.Wai.Internal as W
|
||||
|
||||
getUserR :: UserNameP -> Handler Html
|
||||
getUserR uname=
|
||||
_getUser uname SharedAll FilterAll (TagsP [])
|
||||
|
||||
getUserSharedR :: UserNameP -> SharedP -> Handler Html
|
||||
getUserSharedR uname sharedp =
|
||||
_getUser uname sharedp FilterAll (TagsP [])
|
||||
|
||||
getUserFilterR :: UserNameP -> FilterP -> Handler Html
|
||||
getUserFilterR uname filterp =
|
||||
_getUser uname SharedAll filterp (TagsP [])
|
||||
|
||||
getUserTagsR :: UserNameP -> TagsP -> Handler Html
|
||||
getUserTagsR uname = _getUser uname SharedAll FilterAll
|
||||
|
||||
_getUser :: UserNameP -> SharedP -> FilterP -> TagsP -> Handler Html
|
||||
_getUser unamep@(UserNameP uname) sharedp' filterp' (TagsP pathtags) = do
|
||||
mauthuname <- maybeAuthUsername
|
||||
(limit', page') <- lookupPagingParams
|
||||
let limit = maybe 120 fromIntegral limit'
|
||||
page = maybe 1 fromIntegral page'
|
||||
isowner = 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
|
||||
let mqueryp = fmap (queryp,) mquery
|
||||
(bcount, 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
|
||||
when (bcount == 0) (case filterp of FilterSingle _ -> notFound; _ -> pure ())
|
||||
mroute <- getCurrentRoute
|
||||
tagCloudMode <- getTagCloudMode isowner pathtags
|
||||
req <- getRequest
|
||||
defaultLayout do
|
||||
let pager = $(widgetFile "pager")
|
||||
search = $(widgetFile "search")
|
||||
renderEl = "bookmarks" :: Text
|
||||
tagCloudRenderEl = "tagCloud" :: Text
|
||||
rssLink (UserFeedR unamep) "feed"
|
||||
$(widgetFile "user")
|
||||
toWidgetBody [julius|
|
||||
app.dat.bmarks = #{ toJSON $ toBookmarkFormList btmarks } || [];
|
||||
app.dat.isowner = #{ isowner };
|
||||
app.userR = "@{UserR unamep}";
|
||||
app.tagCloudMode = #{ toJSON $ tagCloudMode } || {};
|
||||
|]
|
||||
toWidget [julius|
|
||||
setTimeout(() => {
|
||||
PS.renderBookmarks('##{rawJS renderEl}')(app.dat.bmarks)();
|
||||
}, 0);
|
||||
setTimeout(() => {
|
||||
PS.renderTagCloud('##{rawJS tagCloudRenderEl}')(app.tagCloudMode)();
|
||||
}, 0);
|
||||
|]
|
||||
|
||||
-- Form
|
||||
|
||||
postUserTagCloudR :: Handler ()
|
||||
postUserTagCloudR = do
|
||||
userId <- requireAuthId
|
||||
mode <- requireCheckJsonBody
|
||||
_updateTagCloudMode mode
|
||||
tc <- runDB $ case mode of
|
||||
TagCloudModeTop _ n -> tagCountTop userId n
|
||||
TagCloudModeLowerBound _ n -> tagCountLowerBound userId n
|
||||
TagCloudModeRelated _ tags -> tagCountRelated userId tags
|
||||
TagCloudModeNone -> notFound
|
||||
sendStatusJSON ok200 (Map.fromList tc :: Map.Map Text Int)
|
||||
|
||||
postUserTagCloudModeR :: Handler ()
|
||||
postUserTagCloudModeR = do
|
||||
userId <- requireAuthId
|
||||
mode <- requireCheckJsonBody
|
||||
_updateTagCloudMode mode
|
||||
|
||||
_updateTagCloudMode :: TagCloudMode -> Handler ()
|
||||
_updateTagCloudMode mode =
|
||||
case mode of
|
||||
TagCloudModeTop _ _ -> setTagCloudMode mode
|
||||
TagCloudModeLowerBound _ _ -> setTagCloudMode mode
|
||||
TagCloudModeRelated _ _ -> setTagCloudMode mode
|
||||
TagCloudModeNone -> notFound
|
||||
|
||||
bookmarkToRssEntry :: (Entity Bookmark, Maybe Text) -> FeedEntry Text
|
||||
bookmarkToRssEntry (Entity entryId entry, tags) =
|
||||
FeedEntry
|
||||
{ feedEntryLink = bookmarkHref entry
|
||||
, 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)
|
||||
|
|
|
@ -48,13 +48,13 @@ aFormToMaybeGetSuccess
|
|||
:: MonadHandler f
|
||||
=> AForm f a -> f (Maybe a)
|
||||
aFormToMaybeGetSuccess =
|
||||
fmap maybeSuccess . fmap fst . runFormGet . const . fmap fst . aFormToForm
|
||||
fmap (maybeSuccess . fst) . runFormGet . const . fmap fst . aFormToForm
|
||||
|
||||
aFormToMaybePostSuccess
|
||||
:: MonadHandlerForm f
|
||||
=> AForm f a -> f (Maybe a)
|
||||
aFormToMaybePostSuccess =
|
||||
fmap maybeSuccess . fmap fst . runFormPostNoToken . const . fmap fst . aFormToForm
|
||||
fmap (maybeSuccess . fst) . runFormPostNoToken . const . fmap fst . aFormToForm
|
||||
|
||||
maybeSuccess :: FormResult a -> Maybe a
|
||||
maybeSuccess (FormSuccess a) = Just a
|
||||
|
@ -83,4 +83,4 @@ attrs n f =
|
|||
}
|
||||
|
||||
cls :: [Text] -> FieldSettings master -> FieldSettings master
|
||||
cls n = attrs [("class", intercalate " " n)]
|
||||
cls n = attrs [("class", unwords n)]
|
||||
|
|
1514
src/Model.hs
1514
src/Model.hs
File diff suppressed because it is too large
Load diff
|
@ -12,6 +12,8 @@ import qualified Data.Aeson as A
|
|||
import System.Entropy (getEntropy)
|
||||
import qualified Data.ByteString.Builder as BB
|
||||
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 size =
|
||||
|
@ -58,3 +60,18 @@ hashPassword rawPassword = do
|
|||
validatePasswordHash :: BCrypt -> T.Text -> Bool
|
||||
validatePasswordHash hash' pass = do
|
||||
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
|
||||
|
|
116
src/PathPiece.hs
116
src/PathPiece.hs
|
@ -1,55 +1,61 @@
|
|||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module PathPiece where
|
||||
|
||||
import Data.Text (splitOn)
|
||||
|
||||
import Import.NoFoundation
|
||||
|
||||
-- PathPiece
|
||||
|
||||
instance PathPiece UserNameP where
|
||||
toPathPiece (UserNameP i) = "u:" <> i
|
||||
fromPathPiece s =
|
||||
case splitOn ":" s of
|
||||
["u", ""] -> Nothing
|
||||
["u", uname] -> Just $ UserNameP uname
|
||||
_ -> Nothing
|
||||
|
||||
instance PathPiece TagsP where
|
||||
toPathPiece (TagsP tags) = "t:" <> (intercalate "+" tags)
|
||||
fromPathPiece s =
|
||||
case splitOn ":" s of
|
||||
["t", ""] -> Nothing
|
||||
["t", tags] -> Just $ TagsP (splitOn "+" tags)
|
||||
_ -> Nothing
|
||||
|
||||
instance PathPiece SharedP where
|
||||
toPathPiece = \case
|
||||
SharedAll -> ""
|
||||
SharedPublic -> "public"
|
||||
SharedPrivate -> "private"
|
||||
fromPathPiece = \case
|
||||
"public" -> Just SharedPublic
|
||||
"private" -> Just SharedPrivate
|
||||
_ -> Nothing
|
||||
|
||||
instance PathPiece FilterP where
|
||||
toPathPiece = \case
|
||||
FilterAll -> ""
|
||||
FilterUnread -> "unread"
|
||||
FilterUntagged -> "untagged"
|
||||
FilterStarred -> "starred"
|
||||
FilterSingle slug -> "b:" <> unBmSlug slug
|
||||
fromPathPiece = \case
|
||||
"unread" -> Just FilterUnread
|
||||
"untagged" -> Just FilterUntagged
|
||||
"starred" -> Just FilterStarred
|
||||
s -> case splitOn ":" s of
|
||||
["b", ""] -> Nothing
|
||||
["b", slug] -> Just $ FilterSingle (BmSlug slug)
|
||||
_ -> Nothing
|
||||
|
||||
|
||||
deriving instance PathPiece NtSlug
|
||||
deriving instance PathPiece BmSlug
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module PathPiece where
|
||||
|
||||
import Data.Text (breakOn, splitOn)
|
||||
import qualified Data.Text as T (replace)
|
||||
import Import.NoFoundation
|
||||
|
||||
-- PathPiece
|
||||
|
||||
instance PathPiece UserNameP where
|
||||
toPathPiece (UserNameP i) = "u:" <> i
|
||||
fromPathPiece s =
|
||||
case breakOn ":" s of
|
||||
("u", "") -> Nothing
|
||||
("u", uname) -> Just $ UserNameP (drop 1 uname)
|
||||
_ -> Nothing
|
||||
|
||||
instance PathPiece TagsP where
|
||||
toPathPiece (TagsP tags) = "t:" <> intercalate "+" (fmap encodeTag tags)
|
||||
fromPathPiece s =
|
||||
case breakOn ":" s of
|
||||
("t", "") -> Nothing
|
||||
("t", tags) -> Just $ (TagsP . fmap decodeTag . splitOn "+" . drop 1) tags
|
||||
_ -> Nothing
|
||||
|
||||
encodeTag :: Text -> Text
|
||||
encodeTag = T.replace "+" "%2B"
|
||||
|
||||
decodeTag :: Text -> Text
|
||||
decodeTag = T.replace "%2B" "+"
|
||||
|
||||
instance PathPiece SharedP where
|
||||
toPathPiece = \case
|
||||
SharedAll -> ""
|
||||
SharedPublic -> "public"
|
||||
SharedPrivate -> "private"
|
||||
fromPathPiece = \case
|
||||
"public" -> Just SharedPublic
|
||||
"private" -> Just SharedPrivate
|
||||
_ -> Nothing
|
||||
|
||||
instance PathPiece FilterP where
|
||||
toPathPiece = \case
|
||||
FilterAll -> ""
|
||||
FilterUnread -> "unread"
|
||||
FilterUntagged -> "untagged"
|
||||
FilterStarred -> "starred"
|
||||
FilterSingle slug -> "b:" <> unBmSlug slug
|
||||
fromPathPiece = \case
|
||||
"unread" -> Just FilterUnread
|
||||
"untagged" -> Just FilterUntagged
|
||||
"starred" -> Just FilterStarred
|
||||
s -> case breakOn ":" s of
|
||||
("b", "") -> Nothing
|
||||
("b", slug) -> Just $ FilterSingle (BmSlug (drop 1 slug))
|
||||
_ -> Nothing
|
||||
|
||||
|
||||
deriving instance PathPiece NtSlug
|
||||
deriving instance PathPiece BmSlug
|
||||
|
|
|
@ -56,16 +56,22 @@ data AppSettings = AppSettings
|
|||
, appAuthDummyLogin :: Bool
|
||||
-- ^ Indicate if auth dummy login should be enabled.
|
||||
|
||||
, appEkgHost :: Maybe Text
|
||||
-- ^ Host/interface the ekg server should bind to.
|
||||
, appEkgPort :: Maybe Int
|
||||
-- ^ Port to listen on
|
||||
, appArchiveSocksProxyHost :: Maybe Text
|
||||
-- ^ Socks proxy host to use when making archive requests
|
||||
|
||||
, appArchiveSocksProxyPort :: Maybe Int
|
||||
-- ^ Socks proxy port to use when making archive requests
|
||||
|
||||
, appSourceCodeUri :: Maybe Text
|
||||
-- ^ Uri to app source code
|
||||
|
||||
, appSSLOnly :: Bool
|
||||
|
||||
, appAllowNonHttpUrlSchemes :: Bool
|
||||
}
|
||||
|
||||
instance FromJSON AppSettings where
|
||||
parseJSON = withObject "AppSettings" $ \o -> do
|
||||
parseJSON = withObject "AppSettings" \o -> do
|
||||
let defaultDev =
|
||||
#ifdef DEVELOPMENT
|
||||
True
|
||||
|
@ -92,9 +98,13 @@ instance FromJSON AppSettings where
|
|||
|
||||
appAuthDummyLogin <- o .:? "auth-dummy-login" .!= dev
|
||||
|
||||
appEkgHost <- o .:? "ekg-host"
|
||||
appEkgPort <- o .:? "ekg-port"
|
||||
appSourceCodeUri <- o .:? "source-code-uri"
|
||||
appArchiveSocksProxyHost <- o .:? "archive-socks-proxy-host"
|
||||
appArchiveSocksProxyPort <- o .:? "archive-socks-proxy-port"
|
||||
appSourceCodeUri <- o .:? "source-code-uri"
|
||||
|
||||
appSSLOnly <- fromMaybe False <$> o .:? "ssl-only"
|
||||
|
||||
appAllowNonHttpUrlSchemes <- fromMaybe False <$> o .:? "allow-non-http-url-schemes"
|
||||
|
||||
return AppSettings {..}
|
||||
|
||||
|
|
10
stack.yaml
10
stack.yaml
|
@ -1,9 +1,3 @@
|
|||
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
|
||||
resolver: lts-20.1
|
||||
packages:
|
||||
- '.'
|
||||
- .
|
||||
|
|
|
@ -3,38 +3,10 @@
|
|||
# For more information, please see the documentation at:
|
||||
# https://docs.haskellstack.org/en/stable/lock_files
|
||||
|
||||
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
|
||||
packages: []
|
||||
snapshots:
|
||||
- completed:
|
||||
size: 523878
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/14/3.yaml
|
||||
sha256: 470c46c27746a48c7c50f829efc0cf00112787a7804ee4ac7a27754658f6d92c
|
||||
original: lts-14.3
|
||||
sha256: b73b2b116143aea728c70e65c3239188998bac5bc3be56465813dacd74215dc5
|
||||
size: 648424
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/1.yaml
|
||||
original: lts-20.1
|
||||
|
|
|
@ -1,194 +1,240 @@
|
|||
html {
|
||||
height: 102%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 102%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
button {
|
||||
background:none;
|
||||
border:none;
|
||||
padding:0;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
[hidden] {
|
||||
display: none !important
|
||||
}
|
||||
input::placeholder {
|
||||
color: lightgray
|
||||
}
|
||||
.queryInput {
|
||||
width: 128px;
|
||||
padding: 0 22px 0 2px;
|
||||
border-radius: 3px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: gray;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
transition: width .1s ease-in-out
|
||||
}
|
||||
.queryInput.search-inactive {}
|
||||
.queryInput:focus {
|
||||
width: 175px;
|
||||
}
|
||||
.submitting .queryInput,
|
||||
.queryInput.search-active {
|
||||
border-color: #990;
|
||||
border-width: 2px;
|
||||
background-color: #FF9;
|
||||
width: 175px;
|
||||
}
|
||||
.queryIcon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top:1px;
|
||||
cursor:pointer;
|
||||
width:20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
}
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
.close-x-wrap {
|
||||
float: left;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
top: 2px;
|
||||
position: relative;
|
||||
right: 2px;
|
||||
}
|
||||
.close-x {
|
||||
stroke: gray;
|
||||
fill: transparent;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 3;
|
||||
}
|
||||
.query-info-icon {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: -18px;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
padding: 0 8px 8px 0;
|
||||
}
|
||||
.star {
|
||||
margin-left:-20px;
|
||||
font-size:1.2em;
|
||||
position:relative;
|
||||
top:-2px;
|
||||
}
|
||||
|
||||
.star button {
|
||||
transition: color .1s;
|
||||
}
|
||||
.star.selected button {
|
||||
color:#22a;
|
||||
}
|
||||
.edit_links button {
|
||||
transition: color .1s ease-in;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color:#a51;
|
||||
line-height:190%;
|
||||
}
|
||||
|
||||
.private { background:#ddd;border:1px solid #d1d1d1; }
|
||||
.unread { color:#b41 }
|
||||
.mark_read {color: #a81;}
|
||||
.flash { color:green;background:#efe }
|
||||
|
||||
.top_menu {
|
||||
margin-top:6px;
|
||||
}
|
||||
.top_menu a {
|
||||
color: blue;
|
||||
}
|
||||
.bookmarklet {
|
||||
padding:1px 2px 0px 2px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
background:#ced;
|
||||
border:1px solid #acc;
|
||||
}
|
||||
|
||||
.edit_bookmark_form {color:#888;}
|
||||
.edit_bookmark_form input {border:1px solid #ddd;}
|
||||
.edit_bookmark_form textarea {border:1px solid #ddd;}
|
||||
|
||||
.nav-active {
|
||||
background:#ff8;
|
||||
color:blue;
|
||||
}
|
||||
|
||||
/* mobile device */
|
||||
@media only screen and (max-width : 750px) {
|
||||
body {
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
.display {
|
||||
float: none
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width : 500px) {
|
||||
.filters {
|
||||
clear: both;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.rdim {
|
||||
opacity: .8;
|
||||
transition: all .15s ease-in;
|
||||
}
|
||||
.rdim:hover,
|
||||
.rdim:focus {
|
||||
opacity: 1;
|
||||
transition: all .15s ease-in;
|
||||
}
|
||||
.display .description > div p,
|
||||
.display .description > div pre
|
||||
{
|
||||
margin-top: 9px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
.display .description > div > *:first-child {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.display .description > div > *:last-child {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.display .description > div > ol li p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.display .description > div > ul li p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.display .description > div ol {
|
||||
padding-left: 23px;
|
||||
}
|
||||
.display .description > div ul {
|
||||
padding-left: 23px;
|
||||
}
|
||||
code, pre {
|
||||
font-size:13px;
|
||||
}
|
||||
|
||||
#content:not([view-rendered]) .view-delay {
|
||||
display: none !important
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html, img, video, iframe { filter: invert(1); }
|
||||
body { background-color: white; }
|
||||
}
|
||||
|
||||
html {
|
||||
height: 102%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 102%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="url"],
|
||||
input[type="password"],
|
||||
textarea {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.queryInput {
|
||||
width: 128px;
|
||||
padding: 0 22px 0 2px;
|
||||
border-radius: 3px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: gray;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
transition: width 0.1s ease-in-out;
|
||||
}
|
||||
.queryInput.search-inactive {
|
||||
}
|
||||
.queryInput:focus {
|
||||
width: 175px;
|
||||
}
|
||||
.submitting .queryInput,
|
||||
.queryInput.search-active {
|
||||
border-color: #990;
|
||||
border-width: 2px;
|
||||
background-color: #ff9;
|
||||
width: 175px;
|
||||
}
|
||||
.queryIcon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 1px;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
}
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
.close-x-wrap {
|
||||
float: left;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
top: 2px;
|
||||
position: relative;
|
||||
right: 2px;
|
||||
}
|
||||
.close-x {
|
||||
stroke: gray;
|
||||
fill: transparent;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 3;
|
||||
}
|
||||
.query-info-icon {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: -18px;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
padding: 0 8px 8px 0;
|
||||
}
|
||||
.star {
|
||||
margin-left: -20px;
|
||||
font-size: 1.2em;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.star button {
|
||||
transition: color 0.1s;
|
||||
}
|
||||
.star.selected button {
|
||||
color: #22a;
|
||||
}
|
||||
.edit_links button {
|
||||
transition: color 0.1s ease-in;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: #a51;
|
||||
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 {
|
||||
background: #ddd;
|
||||
border: 1px solid #d1d1d1;
|
||||
}
|
||||
.unread {
|
||||
color: #b41;
|
||||
}
|
||||
.mark_read {
|
||||
color: #a81;
|
||||
}
|
||||
.flash {
|
||||
color: green;
|
||||
background: #efe;
|
||||
}
|
||||
|
||||
.top_menu {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.top_menu a {
|
||||
color: blue;
|
||||
}
|
||||
.bookmarklet {
|
||||
padding: 1px 2px 0px 2px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
background: #ced;
|
||||
border: 1px solid #acc;
|
||||
margin-bottom: 5px;
|
||||
padding: 2px;
|
||||
}
|
||||
.alert.alert-err {
|
||||
background-color: #ffdfdf
|
||||
}
|
||||
|
||||
.edit_bookmark_form {
|
||||
color: #888;
|
||||
}
|
||||
.edit_bookmark_form input {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.edit_bookmark_form textarea {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.nav-active {
|
||||
background: #ff8;
|
||||
color: blue;
|
||||
}
|
||||
|
||||
/* mobile device */
|
||||
@media only screen and (max-width: 750px) {
|
||||
body {
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
.display {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.filters {
|
||||
clear: both;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.rdim {
|
||||
opacity: 0.8;
|
||||
transition: all 0.15s ease-in;
|
||||
}
|
||||
.rdim:hover,
|
||||
.rdim:focus {
|
||||
opacity: 1;
|
||||
transition: all 0.15s ease-in;
|
||||
}
|
||||
.display .description > div p,
|
||||
.display .description > div pre {
|
||||
margin-top: 9px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
.display .description > div > *:first-child {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.display .description > div > *:last-child {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.display .description > div > ol li p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.display .description > div > ul li p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.display .description > div ol {
|
||||
padding-left: 23px;
|
||||
}
|
||||
.display .description > div ul {
|
||||
padding-left: 23px;
|
||||
}
|
||||
code,
|
||||
pre {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#content:not([view-rendered]) .view-delay {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -1,34 +1,58 @@
|
|||
html {
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
[hidden] {
|
||||
display: none !important
|
||||
display: none !important;
|
||||
}
|
||||
button {
|
||||
background:none;
|
||||
border:none;
|
||||
padding:0;
|
||||
cursor:pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
.alert {
|
||||
background:#ced;
|
||||
border:1px solid #acc;
|
||||
input[type="text"],
|
||||
input[type="url"],
|
||||
textarea {
|
||||
font-size: 16px;
|
||||
}
|
||||
form label {
|
||||
.alert {
|
||||
background: #ced;
|
||||
border: 1px solid #acc;
|
||||
}
|
||||
#addForm .alert {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.alert.alert-err {
|
||||
background-color: #ffdfdf
|
||||
}
|
||||
form label {
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
display: table-cell;
|
||||
padding: 2px 0;
|
||||
}
|
||||
li { list-style-type: none; margin: 0; padding: 0; display: block;}
|
||||
li {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.when { color:#999}
|
||||
.unread { color:#b41 }
|
||||
a.unread { color:#b41 }
|
||||
a.bookmark_title { font-size:120%;}
|
||||
.when {
|
||||
color: #999;
|
||||
}
|
||||
.unread {
|
||||
color: #b41;
|
||||
}
|
||||
a.unread {
|
||||
color: #b41;
|
||||
}
|
||||
a.bookmark_title {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
|
|
80
static/js/app.min.js
vendored
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.
|
@ -1,38 +1,38 @@
|
|||
$newline never
|
||||
\<!doctype html>
|
||||
\<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]-->
|
||||
\<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en"> <![endif]-->
|
||||
\<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en"> <![endif]-->
|
||||
\<!--[if gt IE 8]><!-->
|
||||
<html class="no-js" lang="en"> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<title>#{pageTitle pc}
|
||||
<meta name="description" content="Espial is an open-source, web-based bookmarking server.">
|
||||
<meta name="robots" content="noindex, nofollow, noodp, noydir">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
$maybe sourceCodeUri <- msourceCodeUri
|
||||
<meta name="source" content="#{ sourceCodeUri }">
|
||||
|
||||
^{pageHead pc}
|
||||
|
||||
\<!--[if lt IE 9]>
|
||||
\<script src="@{StaticR js_html5shiv_min_js}"></script>
|
||||
\<![endif]-->
|
||||
|
||||
<script>document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/, 'js');
|
||||
<script src="@{StaticR js_js_cookie_2_2_0_min_js}">
|
||||
<script>
|
||||
var app =
|
||||
{ csrfHeaderName: "#{ TE.decodeUtf8 $ CI.foldedCase defaultCsrfHeaderName }"
|
||||
, csrfParamName: "#{ defaultCsrfParamName }"
|
||||
, csrfCookieName: "#{ TE.decodeUtf8 defaultCsrfCookieName }"
|
||||
, csrfToken: Cookies.get("#{ TE.decodeUtf8 defaultCsrfCookieName }")
|
||||
, homeR: "@{ HomeR }"
|
||||
, authRlogoutR: "@{ AuthR LogoutR }"
|
||||
, userFilterRFilterSingle: ""
|
||||
, dat: {bmarks : [], bmark: {}, isowner: false, notes: []}
|
||||
};
|
||||
<body .f6.dark-gray.helvetica>
|
||||
^{pageBody pc}
|
||||
$newline never
|
||||
\<!doctype html>
|
||||
\<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]-->
|
||||
\<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en"> <![endif]-->
|
||||
\<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en"> <![endif]-->
|
||||
\<!--[if gt IE 8]><!-->
|
||||
<html class="no-js" lang="en"> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<title>#{pageTitle pc}
|
||||
<meta name="description" content="Espial is an open-source, web-based bookmarking server.">
|
||||
<meta name="robots" content="noindex, nofollow, noodp, noydir">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
$maybe sourceCodeUri <- msourceCodeUri
|
||||
<meta name="source" content="#{ sourceCodeUri }">
|
||||
|
||||
^{pageHead pc}
|
||||
|
||||
\<!--[if lt IE 9]>
|
||||
\<script src="@{StaticR js_html5shiv_min_js}"></script>
|
||||
\<![endif]-->
|
||||
|
||||
<script>document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/, 'js');
|
||||
<script src="@{StaticR js_js_cookie_2_2_0_min_js}">
|
||||
<script>
|
||||
var app =
|
||||
{ csrfHeaderName: "#{ TE.decodeUtf8 $ CI.foldedCase defaultCsrfHeaderName }"
|
||||
, csrfParamName: "#{ defaultCsrfParamName }"
|
||||
, csrfCookieName: "#{ TE.decodeUtf8 defaultCsrfCookieName }"
|
||||
, csrfToken: Cookies.get("#{ TE.decodeUtf8 defaultCsrfCookieName }")
|
||||
, homeR: "@{ HomeR }"
|
||||
, authRlogoutR: "@{ AuthR LogoutR }"
|
||||
, userFilterRFilterSingle: ""
|
||||
, dat: {bmarks : [], bmark: {}, isowner: false, notes: []}
|
||||
};
|
||||
<body .f6.dark-gray.helvetica>
|
||||
^{pageBody pc}
|
||||
|
|
|
@ -14,12 +14,11 @@
|
|||
<div .top_menu.fr>
|
||||
|
||||
$maybe userName <- musername
|
||||
$maybe currentroute <- mcurrentRoute
|
||||
<a .link href="@?{(AddViewR, [("next",urlrender currentroute)])}">add url
|
||||
<a .link href="@{AddNoteViewR (UserNameP userName)}">add note
|
||||
<a .link href="@{NotesR (UserNameP userName)}">notes
|
||||
<a .link href="@{AccountSettingsR}">settings
|
||||
<a .link onclick="PS['Main'].logoutE(event)()" href="@{AuthR LogoutR}">
|
||||
<a .link href="@?{(AddViewR, [("next","back")])}">add url
|
||||
<a .link href="@{AddNoteViewR (UserNameP userName)}">add note
|
||||
<a .link href="@{NotesR (UserNameP userName)}">notes
|
||||
<a .link href="@{AccountSettingsR}">settings
|
||||
<a .link onclick="PS.logoutE(event)()" href="@{AuthR LogoutR}">
|
||||
log out
|
||||
$nothing
|
||||
<a .link href="@{AuthR LoginR}">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
- (#{userName})
|
||||
<div .fr>
|
||||
$maybe userName <- musername
|
||||
<a .link onclick="PS['Main'].logoutE(event)()" href="@{AuthR LogoutR}">
|
||||
<a .link onclick="PS.logoutE(event)()" href="@{AuthR LogoutR}">
|
||||
[log out]
|
||||
$nothing
|
||||
<a .link href="@{AuthR LoginR}">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<main #main_column .pv2.ph3.mh1>
|
||||
<div .w-100.mw8.center>
|
||||
<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=350'));">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=360'));">add url bookmarklet
|
||||
|
||||
<a .pa3 href="@{ChangePasswordR}">Change Password
|
||||
|
||||
|
|
|
@ -1,59 +1,71 @@
|
|||
$maybe route <- mroute
|
||||
<main #main_column .pv2.ph3.mh1>
|
||||
<div .w-100.mw8.center>
|
||||
<div .fr.nt1 style="margin-bottom:.7rem">
|
||||
^{search}
|
||||
<div .di>
|
||||
<div .fl.pr3.dib.mb2>
|
||||
<b>
|
||||
<a .link href="@{UserR unamep}">#{uname}
|
||||
$forall tag <- pathtags
|
||||
\ + #
|
||||
<a .link href="@{UserTagsR unamep (TagsP [tag])}">#{tag}
|
||||
<div .fl.pr3.dib.mb2>
|
||||
<span .f7.silver>#{bcount}</span>
|
||||
$if isowner
|
||||
<div .fl.pr3.dib.mb2>
|
||||
<a .link.silver.hover-blue :isAll:.nav-active
|
||||
href="@{UserR unamep}">all
|
||||
‧
|
||||
<a .link.silver.hover-blue :sharedp == SharedPrivate:.nav-active
|
||||
href="@{UserSharedR unamep SharedPrivate}">private
|
||||
‧
|
||||
<a .link.silver.hover-blue :sharedp == SharedPublic:.nav-active
|
||||
href="@{UserSharedR unamep SharedPublic}">public
|
||||
‧
|
||||
<a .link.silver.hover-blue :filterp == FilterUnread:.nav-active
|
||||
href="@{UserFilterR unamep FilterUnread}">unread
|
||||
‧
|
||||
<a .link.silver.hover-blue :filterp == FilterUntagged:.nav-active
|
||||
href="@{UserFilterR unamep FilterUntagged}">untagged
|
||||
‧
|
||||
<a .link.silver.hover-blue :filterp == FilterStarred:.nav-active
|
||||
href="@{UserFilterR unamep FilterStarred}">starred
|
||||
<div .fr.f6.pr3.dib.mb2>
|
||||
<a .link.gold.hover-orange
|
||||
href="@{UserFeedR unamep}">RSS
|
||||
|
||||
<div .cf>
|
||||
|
||||
^{pager}
|
||||
|
||||
<div .cf>
|
||||
|
||||
<div ##{renderEl} .mt3>
|
||||
|
||||
<div .cf>
|
||||
|
||||
<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>
|
||||
|
||||
$maybe route <- mroute
|
||||
<main #main_column .pv2.ph3.mh1>
|
||||
<div .w-100.mw8.center>
|
||||
<div .fr.nt1 style="margin-bottom:.7rem">
|
||||
^{search}
|
||||
<div .di>
|
||||
<div .fl.pr3.dib.mb2>
|
||||
<b>
|
||||
<a .link href="@{UserR unamep}">#{uname}
|
||||
$forall tag <- pathtags
|
||||
\ + #
|
||||
<a .link href="@{UserTagsR unamep (TagsP [tag])}">#{tag}
|
||||
<div .fl.pr3.dib.mb2>
|
||||
<span .f7.silver>#{bcount}</span>
|
||||
$if isowner
|
||||
<div .fl.pr3.dib.mb2>
|
||||
<a .link.silver.hover-blue :isAll:.nav-active
|
||||
href="@{UserR unamep}">all
|
||||
‧
|
||||
<a .link.silver.hover-blue :sharedp == SharedPrivate:.nav-active
|
||||
href="@{UserSharedR unamep SharedPrivate}">private
|
||||
‧
|
||||
<a .link.silver.hover-blue :sharedp == SharedPublic:.nav-active
|
||||
href="@{UserSharedR unamep SharedPublic}">public
|
||||
‧
|
||||
<a .link.silver.hover-blue :filterp == FilterUnread:.nav-active
|
||||
href="@{UserFilterR unamep FilterUnread}">unread
|
||||
‧
|
||||
<a .link.silver.hover-blue :filterp == FilterUntagged:.nav-active
|
||||
href="@{UserFilterR unamep FilterUntagged}">untagged
|
||||
‧
|
||||
<a .link.silver.hover-blue :filterp == FilterStarred:.nav-active
|
||||
href="@{UserFilterR unamep FilterStarred}">starred
|
||||
<div .fr.f6.pr3.dib.mb2>
|
||||
$if sharedp == SharedPrivate
|
||||
<a .link.gold.hover-orange href="@?{(UserFeedSharedR unamep SharedPrivate, catMaybes [mqueryp])}">RSS
|
||||
$elseif sharedp == SharedPublic
|
||||
<a .link.gold.hover-orange href="@?{(UserFeedSharedR unamep SharedPublic, catMaybes [mqueryp])}">RSS
|
||||
$elseif filterp == FilterUnread
|
||||
<a .link.gold.hover-orange href="@?{(UserFeedFilterR unamep FilterUnread, catMaybes [mqueryp])}">RSS
|
||||
$elseif filterp == FilterUntagged
|
||||
<a .link.gold.hover-orange href="@?{(UserFeedFilterR unamep FilterUntagged, catMaybes [mqueryp])}">RSS
|
||||
$elseif filterp == FilterStarred
|
||||
<a .link.gold.hover-orange href="@?{(UserFeedFilterR unamep FilterStarred, catMaybes [mqueryp])}">RSS
|
||||
$else
|
||||
<a .link.gold.hover-orange href="@?{(UserFeedR unamep, catMaybes [mqueryp])}">RSS
|
||||
|
||||
<div .cf>
|
||||
|
||||
^{pager}
|
||||
|
||||
<div .cf>
|
||||
|
||||
<div ##{tagCloudRenderEl}>
|
||||
|
||||
<div ##{renderEl} .mt3>
|
||||
|
||||
<div .cf>
|
||||
|
||||
<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>
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ module TestImport
|
|||
import Application (makeFoundation, makeLogWare)
|
||||
import ClassyPrelude as X hiding (delete, deleteBy, Handler)
|
||||
import Database.Persist as X hiding (get)
|
||||
import Database.Persist.Sql (SqlPersistM, runSqlPersistMPool, rawExecute, rawSql, unSingle, connEscapeName)
|
||||
import Database.Persist.Sql (SqlPersistM, runSqlPersistMPool, rawExecute, rawSql, unSingle)
|
||||
import Foundation as X
|
||||
import Model as X
|
||||
import Test.Hspec as X
|
||||
|
@ -62,8 +62,9 @@ wipeDB app = do
|
|||
|
||||
flip runSqlPersistMPool pool $ do
|
||||
tables <- getTables
|
||||
sqlBackend <- ask
|
||||
let queries = map (\t -> "DELETE FROM " ++ (connEscapeName sqlBackend $ DBName t)) tables
|
||||
-- sqlBackend <- ask
|
||||
-- let queries = map (\t -> "DELETE FROM " ++ (connEscapeName sqlBackend $ DBName t)) tables
|
||||
let queries = map (\t -> "DELETE FROM " ++ t) tables
|
||||
forM_ queries (\q -> rawExecute q [])
|
||||
|
||||
getTables :: DB [Text]
|
||||
|
|
Loading…
Reference in a new issue