diff options
author | Gregor Kleen <gkleen@yggdrasil.li> | 2016-02-24 04:18:03 +0000 |
---|---|---|
committer | Gregor Kleen <gkleen@yggdrasil.li> | 2016-02-24 04:18:03 +0000 |
commit | c09c2786d0654f144dab103292c47411ff1afa9a (patch) | |
tree | 9a49cf5c1a0502ef7382bb54312097c375666ced /webgui | |
parent | c1451ffcbc021e4f25721541538173f24be8f3cc (diff) | |
download | thermoprint-c09c2786d0654f144dab103292c47411ff1afa9a.tar thermoprint-c09c2786d0654f144dab103292c47411ff1afa9a.tar.gz thermoprint-c09c2786d0654f144dab103292c47411ff1afa9a.tar.bz2 thermoprint-c09c2786d0654f144dab103292c47411ff1afa9a.tar.xz thermoprint-c09c2786d0654f144dab103292c47411ff1afa9a.zip |
Partial GUI prototype
Diffstat (limited to 'webgui')
-rw-r--r-- | webgui/data/index.html | 77 | ||||
-rw-r--r-- | webgui/data/style.css | 159 | ||||
-rw-r--r-- | webgui/data/tabs.js | 13 | ||||
-rw-r--r-- | webgui/src/Main.hs | 237 | ||||
-rw-r--r-- | webgui/thermoprint-webgui.cabal | 9 | ||||
-rw-r--r-- | webgui/thermoprint-webgui.nix | 13 |
6 files changed, 468 insertions, 40 deletions
diff --git a/webgui/data/index.html b/webgui/data/index.html new file mode 100644 index 0000000..af780bd --- /dev/null +++ b/webgui/data/index.html | |||
@@ -0,0 +1,77 @@ | |||
1 | <!doctype html> | ||
2 | <html> | ||
3 | <head> | ||
4 | <meta charset="UTF-8" /> | ||
5 | <title>Thermoprint</title> | ||
6 | <link rel="stylesheet" type="text/css" href="static/style.css" /> | ||
7 | <script src="haskell.js"></script> | ||
8 | <script src="static/tabs.js"></script> | ||
9 | <script type="text/javascript" charset="utf-8"> | ||
10 | Haskell.initFFI(); | ||
11 | </script> | ||
12 | </head> | ||
13 | <body> | ||
14 | <p id="javascriptError" class="error fatal"> | ||
15 | Need javascript to function. | ||
16 | </p> | ||
17 | <div class="tabs"> | ||
18 | <ul class="tab-links"> | ||
19 | <li class="active"><a href="#editor">Editor</a></li> | ||
20 | <li><a href="#queue">Queue</a></li> | ||
21 | <li id="errors-tab" style="display:none;"><a href="#errors">Errors</a></li> | ||
22 | </ul> | ||
23 | |||
24 | <div class="tab-content"> | ||
25 | <div class="tab active" id="editor"> | ||
26 | <div class="table" style="width:100%"> | ||
27 | <div class="tr"> | ||
28 | <h1 class="tc">Current Draft</h1> | ||
29 | <h1 class="tc">Saved Drafts</h1> | ||
30 | </div> | ||
31 | <div class="tr"> | ||
32 | <div id="currentDraft" class="tc"> | ||
33 | <!-- <label id="titleLabel" for="editorTitle">Title</label> --> | ||
34 | <input id="editorTitle" /> | ||
35 | <textarea id="editorText"></textarea> | ||
36 | <span id="bbcodeStatus"></span> | ||
37 | <span id="editorStatus">Not connected to server</span> | ||
38 | <div class="table"> | ||
39 | <div class="tr"> | ||
40 | <div class="editorButtonContainer tc"> | ||
41 | <button class="editorButton" id="saveButton">Save</button> | ||
42 | </div> | ||
43 | <div class="editorButtonContainer tc"> | ||
44 | <button class="editorButton" id="printButton">Print</button> | ||
45 | </div> | ||
46 | <div class="editorButtonContainer tc"> | ||
47 | <button class="editorButton" id="discardButton">Discard</button> | ||
48 | </div> | ||
49 | </div> | ||
50 | </div> | ||
51 | </div> | ||
52 | <div class="tc" style="text-align:center;"> | ||
53 | <input id="allowDeletion" type="checkbox" /><label for="allowDeletion">Allow Deletion</label> | ||
54 | <table id="draftList"> | ||
55 | <thead> | ||
56 | <tr> | ||
57 | <td>Id</td> | ||
58 | <td>Title</td> | ||
59 | <td>Actions</td> | ||
60 | </tr> | ||
61 | </thead> | ||
62 | <tbody id="draftListBody"></tbody> | ||
63 | </table> | ||
64 | </div> | ||
65 | </div> | ||
66 | </div> | ||
67 | </div> | ||
68 | |||
69 | <div class="tab" id="queue"> | ||
70 | Blub. | ||
71 | </div> | ||
72 | |||
73 | <ul class="tab" id="errors"> | ||
74 | </ul> | ||
75 | </div> | ||
76 | </body> | ||
77 | </html> | ||
diff --git a/webgui/data/style.css b/webgui/data/style.css new file mode 100644 index 0000000..53ddb7a --- /dev/null +++ b/webgui/data/style.css | |||
@@ -0,0 +1,159 @@ | |||
1 | .table { display: table; } | ||
2 | .tr { display: table-row; } | ||
3 | .tc { display: table-cell; } | ||
4 | |||
5 | .table .table, table .table, .table table, table table { width:100%; height:100%; } | ||
6 | |||
7 | .fatal { | ||
8 | z-index:-1; | ||
9 | position:fixed; | ||
10 | top:50%; | ||
11 | left:50%; | ||
12 | transform: translate(-50%, -50%); | ||
13 | background-color:#fdd; | ||
14 | border:2px solid #c00; | ||
15 | text-align:center; | ||
16 | vertical-align:center; | ||
17 | padding:1em; | ||
18 | font-weight:600; | ||
19 | } | ||
20 | |||
21 | h1 { | ||
22 | font-size:1.5em; | ||
23 | font-weight:normal; | ||
24 | text-align:center; | ||
25 | } | ||
26 | |||
27 | thead td { | ||
28 | font-size:1em; | ||
29 | font-weight:600; | ||
30 | text-align:center; | ||
31 | } | ||
32 | |||
33 | thead tr:last-child { | ||
34 | padding-bottom:0.125em; | ||
35 | border-bottom:1px solid #ddd; | ||
36 | margin-bottom:0.125em; | ||
37 | } | ||
38 | |||
39 | .editorButtonContainer { | ||
40 | text-align:center; | ||
41 | } | ||
42 | |||
43 | .editorButton { | ||
44 | display:inline-block; | ||
45 | width:8em; | ||
46 | } | ||
47 | |||
48 | #editorTitle { | ||
49 | width:100%; | ||
50 | box-sizing:border-box; | ||
51 | margin:0 0 0.25em 0; | ||
52 | } | ||
53 | |||
54 | #editorText { | ||
55 | width:100%; | ||
56 | box-sizing:border-box; | ||
57 | margin:0 0 0.25em 0; | ||
58 | resize:vertical; | ||
59 | min-height:25em; | ||
60 | } | ||
61 | |||
62 | #editorStatus { | ||
63 | display:block; | ||
64 | margin:0 0 0.25em 0; | ||
65 | font-size:0.75em; | ||
66 | } | ||
67 | |||
68 | #editorStatus:after { | ||
69 | display:block; | ||
70 | clear:both; | ||
71 | content:''; | ||
72 | } | ||
73 | |||
74 | #bbcodeStatus { | ||
75 | display:block; | ||
76 | margin:0 0 0.25em 0; | ||
77 | font-size:0.75em; | ||
78 | float:right; | ||
79 | color:#c00000; | ||
80 | } | ||
81 | |||
82 | /*----- Tabs -----*/ | ||
83 | .tabs { | ||
84 | display:block; | ||
85 | width:98%; | ||
86 | min-height:98%; | ||
87 | position:absolute; | ||
88 | left:1%; | ||
89 | top:1%; | ||
90 | } | ||
91 | |||
92 | /*----- Tab Links -----*/ | ||
93 | .tab-links { | ||
94 | display:block; | ||
95 | height:2.2em; | ||
96 | margin:0; | ||
97 | padding:0 0.5em; | ||
98 | z-index:0; | ||
99 | } | ||
100 | |||
101 | /* Clearfix */ | ||
102 | .tab-links:after { | ||
103 | display:block; | ||
104 | clear:both; | ||
105 | content:''; | ||
106 | } | ||
107 | |||
108 | .tab-links li { | ||
109 | margin:0px 0.5em; | ||
110 | float:left; | ||
111 | list-style:none; | ||
112 | } | ||
113 | |||
114 | .tab-links a { | ||
115 | padding:0.5em 1em; | ||
116 | display:inline-block; | ||
117 | border-radius:3px 3px 0px 0px; | ||
118 | background:#fff; | ||
119 | font-size:1em; | ||
120 | font-weight:600; | ||
121 | color:#4c4c4c; | ||
122 | transition:all linear 0.15s; | ||
123 | } | ||
124 | |||
125 | .tab-links a:hover { | ||
126 | text-decoration:none; | ||
127 | } | ||
128 | |||
129 | li.active a, li.active a:hover { | ||
130 | background:#f0f0f0; | ||
131 | color:#4c4c4c; | ||
132 | text-decoration:none; | ||
133 | } | ||
134 | |||
135 | #errors-tab a { | ||
136 | color:#c00000; | ||
137 | } | ||
138 | |||
139 | /*----- Content of Tabs -----*/ | ||
140 | .tab-content { | ||
141 | padding:0.5em; | ||
142 | border:1px solid #ddd; | ||
143 | border-radius:3px; | ||
144 | box-shadow:-1px 1px 1px rgba(0,0,0,0.15); | ||
145 | background:#fff; | ||
146 | box-sizing:border-box; | ||
147 | width:100%; | ||
148 | z-index:1; | ||
149 | margin-top:-1px; | ||
150 | margin-bottom:1%; | ||
151 | } | ||
152 | |||
153 | .tab { | ||
154 | display:none; | ||
155 | } | ||
156 | |||
157 | .tab.active { | ||
158 | display:block; | ||
159 | } \ No newline at end of file | ||
diff --git a/webgui/data/tabs.js b/webgui/data/tabs.js new file mode 100644 index 0000000..1d178d9 --- /dev/null +++ b/webgui/data/tabs.js | |||
@@ -0,0 +1,13 @@ | |||
1 | jQuery(document).ready(function() { | ||
2 | jQuery('.tabs .tab-links a').on('click', function(e) { | ||
3 | var currentAttrValue = jQuery(this).attr('href'); | ||
4 | |||
5 | // Show/Hide Tabs | ||
6 | jQuery('.tabs ' + currentAttrValue).show().siblings().hide(); | ||
7 | |||
8 | // Change/remove current tab to active | ||
9 | jQuery(this).parent('li').addClass('active').siblings().removeClass('active'); | ||
10 | |||
11 | e.preventDefault(); | ||
12 | }); | ||
13 | }); | ||
diff --git a/webgui/src/Main.hs b/webgui/src/Main.hs index 92aba0e..44bcb88 100644 --- a/webgui/src/Main.hs +++ b/webgui/src/Main.hs | |||
@@ -4,10 +4,13 @@ | |||
4 | module Main (main) where | 4 | module Main (main) where |
5 | 5 | ||
6 | import qualified Graphics.UI.Threepenny as UI | 6 | import qualified Graphics.UI.Threepenny as UI |
7 | import qualified Graphics.UI.Threepenny.Core as TP (Config) | 7 | import Graphics.UI.Threepenny.Timer |
8 | import Graphics.UI.Threepenny.Core hiding (Config) | 8 | import qualified Graphics.UI.Threepenny.Core as TP (Config, text) |
9 | import Graphics.UI.Threepenny.Core hiding (Config, text) | ||
9 | 10 | ||
10 | import Thermoprint.Client | 11 | import Thermoprint.Client |
12 | import Thermoprint.Printout.BBCode | ||
13 | |||
11 | import Network.WebSockets | 14 | import Network.WebSockets |
12 | import Network.Socket (withSocketsDo) | 15 | import Network.Socket (withSocketsDo) |
13 | import Network.URI | 16 | import Network.URI |
@@ -22,6 +25,15 @@ import qualified Data.ByteString.Char8 as CBS | |||
22 | import Data.Text (Text) | 25 | import Data.Text (Text) |
23 | import qualified Data.Text as T | 26 | import qualified Data.Text as T |
24 | 27 | ||
28 | import Data.Map (Map) | ||
29 | import qualified Data.Map as Map | ||
30 | import Data.Sequence (Seq) | ||
31 | import qualified Data.Sequence as Seq | ||
32 | |||
33 | import Data.Default.Class | ||
34 | |||
35 | import Data.Time | ||
36 | |||
25 | import Control.Concurrent | 37 | import Control.Concurrent |
26 | import Control.Exception | 38 | import Control.Exception |
27 | import Control.Monad.Catch | 39 | import Control.Monad.Catch |
@@ -30,7 +42,13 @@ import Control.Applicative | |||
30 | import Control.Monad | 42 | import Control.Monad |
31 | import Data.Maybe | 43 | import Data.Maybe |
32 | import Data.Monoid | 44 | import Data.Monoid |
33 | import Text.Read | 45 | import Text.Read hiding (get) |
46 | import Data.Either | ||
47 | import Data.List (isPrefixOf) | ||
48 | |||
49 | import Paths_thermoprint_webgui | ||
50 | |||
51 | import Debug.Trace | ||
34 | 52 | ||
35 | data Config = Config | 53 | data Config = Config |
36 | { tpConfig :: TP.Config | 54 | { tpConfig :: TP.Config |
@@ -49,7 +67,13 @@ config :: IO (Opt.Parser Config) | |||
49 | config = do | 67 | config = do |
50 | p <- fromMaybe 8023 . (readMaybe =<<) <$> lookupEnv portEnv | 68 | p <- fromMaybe 8023 . (readMaybe =<<) <$> lookupEnv portEnv |
51 | a <- fromMaybe "localhost" <$> lookupEnv hostEnv | 69 | a <- fromMaybe "localhost" <$> lookupEnv hostEnv |
52 | return $ Config <$> ((\port addr -> defaultConfig { jsPort = port, jsAddr = addr }) <$> optional (Opt.option Opt.auto $ port p) <*> optional (fmap CBS.pack $ Opt.strOption $ addr a)) | 70 | static <- getDataDir |
71 | let | ||
72 | config = defaultConfig | ||
73 | { jsCustomHTML = Just "index.html" | ||
74 | , jsStatic = Just static | ||
75 | } | ||
76 | return $ Config <$> ((\port addr -> config { jsPort = port, jsAddr = addr }) <$> optional (Opt.option Opt.auto $ port p) <*> optional (fmap CBS.pack $ Opt.strOption $ addr a)) | ||
53 | <*> (BaseUrl Http | 77 | <*> (BaseUrl Http |
54 | <$> Opt.strOption (Opt.long "target-addr" <> Opt.short 'A' <> Opt.metavar "HOST" <> Opt.help "Host to connect to" <> Opt.value "localhost" <> Opt.showDefault) | 78 | <$> Opt.strOption (Opt.long "target-addr" <> Opt.short 'A' <> Opt.metavar "HOST" <> Opt.help "Host to connect to" <> Opt.value "localhost" <> Opt.showDefault) |
55 | <*> Opt.option Opt.auto (Opt.long "target-port" <> Opt.short 'P' <> Opt.metavar "PORT" <> Opt.help "Port to connect to" <> Opt.value 3000 <> Opt.showDefault) | 79 | <*> Opt.option Opt.auto (Opt.long "target-port" <> Opt.short 'P' <> Opt.metavar "PORT" <> Opt.help "Port to connect to" <> Opt.value 3000 <> Opt.showDefault) |
@@ -74,37 +98,182 @@ setup :: Config -> Window -> Event (Either WebSocketException URI) -> UI () | |||
74 | setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | 98 | setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do |
75 | onEvent socketErr handleSocketErr | 99 | onEvent socketErr handleSocketErr |
76 | 100 | ||
77 | return window # set UI.title "Thermoprint" | 101 | getElementById window "javascriptError" >>= maybeM delete |
78 | 102 | ||
79 | title <- UI.input | 103 | changeEditorStatus <- handleEditor |
80 | # set UI.id_ "title" | 104 | |
81 | content <- UI.textarea | 105 | handleDraftTable changeEditorStatus |
82 | # set UI.id_ "content" | ||
83 | draftTable <- UI.table | ||
84 | |||
85 | saveBtn <- UI.button #+ [string "Save"] | ||
86 | discBtn <- UI.button #+ [string "Discard"] | ||
87 | prntBtn <- UI.button #+ [string "Print"] | ||
88 | |||
89 | getBody window #+ [ row [ column [ UI.h1 #+ [string "Current draft"] | ||
90 | , row [UI.label # set text "Title" # set UI.for "title", element title] | ||
91 | , element content | ||
92 | , row [ element saveBtn | ||
93 | , element prntBtn | ||
94 | , element discBtn | ||
95 | ] | ||
96 | ] | ||
97 | , column [ UI.h1 #+ [string "Saved drafts"] | ||
98 | , element draftTable | ||
99 | ] | ||
100 | ] | ||
101 | ] | ||
102 | 106 | ||
103 | where | 107 | where |
104 | handleSocketErr InvalidMessage = debug "Received unparseable message from websocket" | 108 | handleSocketErr InvalidMessage = emitError "Received unparseable message from server-side websocket" |
105 | handleSocketErr e@(Unhandled e') = void $ fatalError (show e') >> liftIO (throwIO e) | 109 | handleSocketErr ProcessDied = fatal "Thread handling server-side websocket died" |
106 | fatalError str = (getBody window #) . set children =<< sequence [UI.p # set text str # set UI.id_ "fatal-error"] | 110 | handleSocketErr (Unhandled e') = fatal $ "Unhandled error from server-side websocket: " ++ show e' |
111 | emitError str = void $ do | ||
112 | debug str | ||
113 | let | ||
114 | errors = maybe (fatal "No container for nonfatal errors found") return =<< getElementById window "errors" | ||
115 | errorsTab = maybe (fatal "Could not make nonfatal errors visible") return =<< getElementById window "errors-tab" | ||
116 | errors #+ [UI.li # set TP.text str] | ||
117 | errorsTab # set style [("display", "inline-block")] | ||
118 | fatal :: String -> UI a | ||
119 | fatal str = do | ||
120 | (getBody window #) . set children =<< sequence [UI.p # set TP.text str # set UI.class_ "fatal"] | ||
121 | liftIO (throwIO $ ErrorCall str) | ||
122 | return undefined | ||
123 | |||
124 | maybeM = maybe $ return () | ||
125 | |||
126 | fatal' :: String -> Maybe a -> UI a | ||
127 | fatal' str = maybe (fatal str) return | ||
128 | |||
129 | stepper' :: a -> UI (Behavior a, a -> IO ()) | ||
130 | stepper' init = do | ||
131 | (statusEvent, triggerStatusChange) <- liftIO newEvent | ||
132 | status <- stepper init statusEvent | ||
133 | return (status, triggerStatusChange) | ||
134 | |||
135 | Client{..} = mkClient (Nat $ either (fatal . ("Error while communicating to Thermoprint.Server: " ++) . show) return <=< liftIO . runEitherT) server | ||
136 | |||
137 | handleEditor = do | ||
138 | title <- fatal' "Could not find editor title field" =<< getElementById window "editorTitle" | ||
139 | text <- fatal' "Could not find editor text field" =<< getElementById window "editorText" | ||
140 | status <- fatal' "Could not find editor status field" =<< getElementById window "editorStatus" | ||
141 | bbcodeStatus <- fatal' "Could not find editor bbcode status field" =<< getElementById window "bbcodeStatus" | ||
142 | |||
143 | saveButton <- fatal' "Could not find editor save button" =<< getElementById window "saveButton" | ||
144 | printButton <- fatal' "Could not find editor print button" =<< getElementById window "printButton" | ||
145 | discardButton <- fatal' "Could not find editor discard button" =<< getElementById window "discardButton" | ||
146 | |||
147 | (editorStatus, fmap liftIO -> changeEditorStatus) <- stepper' def | ||
148 | |||
149 | let | ||
150 | modifyStatus f = changeEditorStatus . f =<< currentValue editorStatus | ||
151 | |||
152 | on UI.valueChange title $ \str -> modifyStatus (\x -> x { eTitle = if null str then Nothing else Just str }) | ||
153 | on UI.valueChange text $ \str -> modifyStatus (\x -> x { eText = str }) | ||
154 | on UI.valueChange text $ \str -> modifyStatus (\x -> x { ePrintout = bbcode $ T.pack str }) | ||
107 | 155 | ||
156 | return title # sink UI.value (fromMaybe "" . eTitle <$> editorStatus) | ||
157 | return text # sink UI.text (eText <$> editorStatus) | ||
158 | return status # sink TP.text (toStatusString <$> editorStatus) | ||
159 | return bbcodeStatus # sink TP.text (toStatusString' . ePrintout <$> editorStatus) | ||
160 | |||
161 | autoSaveTimer <- timer # set interval 5000 | ||
162 | start autoSaveTimer | ||
163 | |||
164 | let | ||
165 | saveAction automatic = do | ||
166 | s@(EditorState{..}) <- currentValue editorStatus | ||
167 | when (not $ maybe True null eTitle && null eText) $ case ePrintout of | ||
168 | Left err -> when (not automatic) . emitError $ "Could not save draft due to error parsing bbcode: " ++ show err | ||
169 | Right p -> do | ||
170 | draftId <- case associatedDraft of | ||
171 | Nothing -> draftCreate (T.pack <$> eTitle) p | ||
172 | Just i -> i <$ when (different s) (draftReplace i (T.pack <$> eTitle) p) | ||
173 | time <- liftIO getCurrentTime | ||
174 | modifyStatus (\x -> x { associatedDraft = Just draftId, lastSaved = Just (time, s) }) | ||
175 | where | ||
176 | different s | ||
177 | | Just (_, s') <- lastSaved s = not $ and [ eTitle s == eTitle s' | ||
178 | , eText s == eText s' | ||
179 | ] | ||
180 | | otherwise = True | ||
181 | discardAction = do | ||
182 | maybe (return ()) draftDelete . associatedDraft =<< currentValue editorStatus | ||
183 | modifyStatus $ const def | ||
184 | printAction = do | ||
185 | emitError "Printing not implemented" | ||
186 | |||
187 | onEvent (tick autoSaveTimer) (const $ saveAction True) | ||
188 | |||
189 | return saveButton # sink UI.enabled (saveable <$> editorStatus) | ||
190 | return printButton # sink UI.enabled (printable <$> editorStatus) | ||
191 | return discardButton # sink UI.enabled (discardable <$> editorStatus) | ||
192 | |||
193 | on UI.click saveButton . const $ saveAction False | ||
194 | on UI.click printButton $ const printAction | ||
195 | on UI.click discardButton $ const discardAction | ||
196 | |||
197 | return changeEditorStatus | ||
198 | |||
199 | saveable s@EditorState{..} = isRight ePrintout && discardable s | ||
200 | printable EditorState{..} = isRight ePrintout && not (null eText) | ||
201 | discardable EditorState{..} = not (maybe True null eTitle && null eText) | ||
202 | |||
203 | handleDraftTable changeEditorState = do | ||
204 | allowDeletion <- fatal' "Could not find deletion switch" =<< getElementById window "allowDeletion" | ||
205 | deletion' <- allowDeletion # get UI.checked | ||
206 | |||
207 | deletion <- stepper deletion' $ UI.checkedChange allowDeletion | ||
208 | let | ||
209 | toTable :: Map DraftId (Maybe DraftTitle) -> UI [Element] | ||
210 | toTable = mapM toLine . Map.toList | ||
211 | |||
212 | toLine (id@(show -> tId), fromMaybe "" . fmap T.unpack -> title) = do | ||
213 | id' <- UI.td # set TP.text tId | ||
214 | title' <- UI.td # set TP.text title | ||
215 | delete <- UI.button | ||
216 | # set TP.text "Delete" | ||
217 | # sink UI.enabled deletion | ||
218 | on UI.click delete . const $ draftDelete id | ||
219 | load <- UI.button | ||
220 | # set TP.text "Load" | ||
221 | on UI.click load . const $ loadDraft id | ||
222 | actions <- UI.td # set children [load, delete] | ||
223 | UI.tr # set children [id', title', actions] | ||
224 | loadDraft id = do | ||
225 | (title, po) <- draft id | ||
226 | time <- liftIO $ getCurrentTime | ||
227 | let newState = def | ||
228 | { eTitle = fmap T.unpack title | ||
229 | , eText = "unimplemented" -- TODO: implement -- drops data as it is | ||
230 | , ePrintout = Right po | ||
231 | , associatedDraft = Just id | ||
232 | , lastSaved = Just (time, newState) | ||
233 | } | ||
234 | changeEditorState newState | ||
235 | table <- fatal' "Could not find draft table" =<< getElementById window "draftListBody" | ||
236 | initialContent <- toTable =<< drafts | ||
237 | return table # set children initialContent | ||
238 | |||
239 | onEvent (filterE concernsDrafts dataUpdate) . const $ drafts >>= toTable >>= (\c -> return table # set children c) | ||
240 | where | ||
241 | concernsDrafts :: URI -> Bool | ||
242 | concernsDrafts (uriPath -> p) | ||
243 | | p == "drafts" = True | ||
244 | | "draft/" `isPrefixOf` p = True | ||
245 | | otherwise = False | ||
246 | |||
247 | |||
248 | data EditorState = EditorState | ||
249 | { eTitle :: Maybe String | ||
250 | , eText :: String | ||
251 | , ePrintout :: Either BBCodeError Printout | ||
252 | , lastSaved :: Maybe (UTCTime, EditorState) | ||
253 | , associatedDraft :: Maybe DraftId | ||
254 | } | ||
255 | deriving (Show) | ||
256 | |||
257 | instance Default EditorState where | ||
258 | def = EditorState | ||
259 | { eTitle = Nothing | ||
260 | , eText = "" | ||
261 | , ePrintout = Right mempty | ||
262 | , lastSaved = Nothing | ||
263 | , associatedDraft = Nothing | ||
264 | } | ||
265 | |||
266 | toStatusString :: EditorState -> String | ||
267 | toStatusString EditorState{..} | ||
268 | | null eTitle | ||
269 | , null eText = "Draft is empty" | ||
270 | | Just (t, _) <- lastSaved = "Last saved: " ++ formatTime defaultTimeLocale "%F %X" t | ||
271 | | otherwise = "Draft was never saved" | ||
272 | |||
273 | toStatusString' :: Either BBCodeError a -> String | ||
274 | toStatusString' (Right _) = "" | ||
275 | toStatusString' (Left e) = show e | ||
276 | |||
108 | data WebSocketException = ProcessDied | InvalidMessage | Unhandled SomeException | 277 | data WebSocketException = ProcessDied | InvalidMessage | Unhandled SomeException |
109 | deriving (Show) | 278 | deriving (Show) |
110 | 279 | ||
@@ -114,7 +283,7 @@ withWebSocket :: (Config -> Window -> Event (Either WebSocketException URI) -> U | |||
114 | withWebSocket setup c@(Config{..}) w = do | 283 | withWebSocket setup c@(Config{..}) w = do |
115 | (dataUpdate, triggerData) <- liftIO newEvent | 284 | (dataUpdate, triggerData) <- liftIO newEvent |
116 | let | 285 | let |
117 | rcvEvents = runClient (baseUrlHost server) (baseUrlPort server) "/" $ forever . (triggerData . maybe (Left InvalidMessage) Right . parseURI . T.unpack <=< receiveData) | 286 | rcvEvents = runClient (baseUrlHost server) (baseUrlPort server) "/" $ forever . (triggerData . maybe (Left InvalidMessage) Right . parseURIReference . T.unpack <=< receiveData) |
118 | liftIOLater . void $ forkFinally (rcvEvents `catchAll` (triggerData . Left . Unhandled)) (triggerData $ Left ProcessDied) | 287 | liftIOLater . void $ forkFinally rcvEvents (triggerData . Left . either Unhandled (const ProcessDied)) |
119 | void $ setup c w dataUpdate | 288 | void $ setup c w dataUpdate |
120 | 289 | ||
diff --git a/webgui/thermoprint-webgui.cabal b/webgui/thermoprint-webgui.cabal index c1f4d19..b76175d 100644 --- a/webgui/thermoprint-webgui.cabal +++ b/webgui/thermoprint-webgui.cabal | |||
@@ -16,6 +16,11 @@ build-type: Simple | |||
16 | -- extra-source-files: | 16 | -- extra-source-files: |
17 | cabal-version: >=1.10 | 17 | cabal-version: >=1.10 |
18 | 18 | ||
19 | data-dir: data | ||
20 | data-files: index.html | ||
21 | , style.css | ||
22 | , tabs.js | ||
23 | |||
19 | executable thermoprint-webgui | 24 | executable thermoprint-webgui |
20 | main-is: Main.hs | 25 | main-is: Main.hs |
21 | -- other-modules: | 26 | -- other-modules: |
@@ -31,5 +36,9 @@ executable thermoprint-webgui | |||
31 | , network-uri >=2.6.0 && <3 | 36 | , network-uri >=2.6.0 && <3 |
32 | , text >=1.2.2 && <2 | 37 | , text >=1.2.2 && <2 |
33 | , exceptions >=0.8.2 && <1 | 38 | , exceptions >=0.8.2 && <1 |
39 | , containers >=0.5.6 && <1 | ||
40 | , either >=4.4.1 && <5 | ||
41 | , time >=1.5.0 && <2 | ||
42 | , data-default-class >=0.0 && <1 | ||
34 | hs-source-dirs: src | 43 | hs-source-dirs: src |
35 | default-language: Haskell2010 \ No newline at end of file | 44 | default-language: Haskell2010 \ No newline at end of file |
diff --git a/webgui/thermoprint-webgui.nix b/webgui/thermoprint-webgui.nix index 5eb5912..457f39a 100644 --- a/webgui/thermoprint-webgui.nix +++ b/webgui/thermoprint-webgui.nix | |||
@@ -1,6 +1,7 @@ | |||
1 | { mkDerivation, base, bytestring, exceptions, network, network-uri | 1 | { mkDerivation, base, bytestring, containers, data-default-class |
2 | , optparse-applicative, stdenv, text, thermoprint-bbcode | 2 | , either, exceptions, network, network-uri, optparse-applicative |
3 | , thermoprint-client, threepenny-gui, websockets | 3 | , stdenv, text, thermoprint-bbcode, thermoprint-client |
4 | , threepenny-gui, time, websockets | ||
4 | }: | 5 | }: |
5 | mkDerivation { | 6 | mkDerivation { |
6 | pname = "thermoprint-webgui"; | 7 | pname = "thermoprint-webgui"; |
@@ -9,9 +10,9 @@ mkDerivation { | |||
9 | isLibrary = false; | 10 | isLibrary = false; |
10 | isExecutable = true; | 11 | isExecutable = true; |
11 | executableHaskellDepends = [ | 12 | executableHaskellDepends = [ |
12 | base bytestring exceptions network network-uri optparse-applicative | 13 | base bytestring containers data-default-class either exceptions |
13 | text thermoprint-bbcode thermoprint-client threepenny-gui | 14 | network network-uri optparse-applicative text thermoprint-bbcode |
14 | websockets | 15 | thermoprint-client threepenny-gui time websockets |
15 | ]; | 16 | ]; |
16 | homepage = "http://dirty-haskell.org/tags/thermoprint.html"; | 17 | homepage = "http://dirty-haskell.org/tags/thermoprint.html"; |
17 | description = "Threepenny interface for thermoprint-spec compliant servers"; | 18 | description = "Threepenny interface for thermoprint-spec compliant servers"; |