module Main exposing (..)
import Browser
import Browser.Events
import Element exposing (..)
import Element.Background as Background
import Element.Border as Border
import Element.Events
import Element.Font as Font
import Element.Input as Input
import Html exposing (Html)
import Http
import Ionicon
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
type FileType
= Pdf
| Jpeg
type Dialog
= SaveOrDownloadPdf
| SaveOrDownloadJpeg
| PdfSaved
| JpegSaved
| NoDialog
type alias Model =
{ viewportWidth : Int
, viewportHeight : Int
, appUrl : String
, scanner : String
, scannerReady : Bool
, images : List String
, selectedImage : Maybe Int
, pdfFile : Maybe String
, dialog : Dialog
}
init : { viewportWidth : Int, viewportHeight : Int, url : String } -> (Model, Cmd Msg)
init browserData =
let appUrl = browserData.url ++ "app/" in
( { viewportWidth = browserData.viewportWidth
, viewportHeight = browserData.viewportHeight
, appUrl = appUrl
, scanner = ""
, scannerReady = False
, images = []
, selectedImage = Nothing
, pdfFile = Nothing
, dialog = NoDialog
}
, Http.get
{ url = appUrl ++ "get-scanner"
, expect = Http.expectString AddScanner
}
)
type Msg
= UpdateViewport (Int, Int)
| AddScanner (Result Http.Error String)
| ScanImage
| AddImage (Result Http.Error String)
| SelectImage Int
| RemoveImage
| ImageUp
| ImageDown
| RotateImage
| ReplaceImage (Result Http.Error String)
| SaveImage
| MakePdf
| SavePdf String
| ShowSaveOrDownloadPdfDialog (Result Http.Error String)
| ShowSaveOrDownloadJpegDialog
| ShowPdfSavedDialog (Result Http.Error String)
| ShowJpegSavedDialog (Result Http.Error String)
| ClearDialog
| Noop (Result Http.Error String)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
let
splitList3 : Int -> Int -> List a -> (List a, List a, List a)
splitList3 middleStart middleLength list =
( List.take middleStart list
, List.drop middleStart list |> List.take middleLength
, List.drop (middleStart + middleLength) list
)
in
case msg of
UpdateViewport (width, height) ->
( { model | viewportWidth = width, viewportHeight = height }
, Cmd.none
)
AddScanner result ->
case result of
Ok scanner ->
( { model | scanner = scanner, scannerReady = True }
, Cmd.none
)
Err _ -> ( model, Cmd.none )
ScanImage ->
case model.scannerReady of
True ->
( { model | scannerReady = False }
, Http.get
{ url = model.appUrl ++ "scan/" ++ model.scanner
, expect = Http.expectString AddImage
}
)
False -> ( model, Cmd.none )
AddImage result ->
case result of
Ok image ->
( { model | scannerReady = True, images = model.images ++ [image] }
, Cmd.none
)
Err _ -> ( { model | scannerReady = True }, Cmd.none )
SelectImage index ->
( { model | selectedImage = Just index }, Cmd.none )
RemoveImage ->
case model.selectedImage of
Just index ->
let
(listA, _, listC) = splitList3 index 1 model.images
newImages = listA ++ listC
in
( { model | images = newImages, selectedImage = Nothing }, Cmd.none )
Nothing -> ( model, Cmd.none )
ImageUp ->
case model.selectedImage of
Just index ->
if index /= 0 then
let
(listA, listB, listC) = splitList3 (index - 1) 2 model.images
newImages = listA ++ (List.reverse listB) ++ listC
in
( { model | images = newImages, selectedImage = Nothing }, Cmd.none )
else ( model, Cmd.none )
Nothing -> ( model, Cmd.none )
ImageDown ->
case model.selectedImage of
Just index ->
if index /= (List.length model.images) - 1 then
let
(listA, listB, listC) = splitList3 index 2 model.images
newImages = listA ++ (List.reverse listB) ++ listC
in
( { model | images = newImages, selectedImage = Nothing }, Cmd.none )
else ( model, Cmd.none )
Nothing -> ( model, Cmd.none )
RotateImage ->
case model.scannerReady of
True ->
case model.selectedImage of
Just index ->
case List.head <| List.drop index model.images of
Just value ->
( { model | scannerReady = False }
, Http.get
{ url = model.appUrl ++ "rotate/" ++ value
, expect = Http.expectString ReplaceImage
}
)
Nothing -> ( model, Cmd.none )
Nothing -> ( model, Cmd.none )
False -> ( model, Cmd.none )
ReplaceImage result ->
case result of
Ok image ->
case model.selectedImage of
Just index ->
let
(listA, _, listC) = splitList3 index 1 model.images
newImages = listA ++ [image] ++ listC
in
( { model | scannerReady = True, images = newImages }, Cmd.none )
Nothing -> ( model, Cmd.none )
Err _ -> ( { model | scannerReady = True }, Cmd.none )
SaveImage ->
case model.selectedImage of
Just index ->
case List.head <| List.drop index model.images of
Just value ->
( model
, Http.get
{ url = model.appUrl ++ "save/" ++ value
, expect = Http.expectString ShowJpegSavedDialog
}
)
Nothing -> ( model, Cmd.none )
Nothing -> ( model, Cmd.none )
MakePdf ->
let path = model.images |> List.intersperse "/" |> List.foldr (++) "" in
( model
, Http.get
{ url = model.appUrl ++ "pdf/" ++ path
, expect = Http.expectString ShowSaveOrDownloadPdfDialog
}
)
SavePdf fileName ->
( model
, Http.get
{ url = model.appUrl ++ "save/" ++ fileName
, expect = Http.expectString ShowPdfSavedDialog
}
)
ShowSaveOrDownloadPdfDialog result ->
case result of
Ok fileName ->
( { model | dialog = SaveOrDownloadPdf, pdfFile = Just fileName }, Cmd.none )
Err _ -> ( model, Cmd.none )
ShowSaveOrDownloadJpegDialog ->
( { model | dialog = SaveOrDownloadJpeg }, Cmd.none )
ShowPdfSavedDialog _ ->
( { model | dialog = PdfSaved }, Cmd.none )
ShowJpegSavedDialog _ ->
( { model | dialog = JpegSaved }, Cmd.none )
ClearDialog ->
( { model | dialog = NoDialog, pdfFile = Nothing }, Cmd.none )
Noop _ ->
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions _ =
Browser.Events.onResize (\w h -> UpdateViewport (w, h))
type alias RGBA =
{ red : Float
, green : Float
, blue : Float
, alpha : Float
}
primaryButtonColor = rgb255 150 150 255
secondaryButtonColor = rgb255 25 175 25
headerColor = rgb255 230 230 230
mainBackgroundColor = rgb255 50 50 50
primaryTextColor = rgb255 255 255 255
storageName = "LeeData"
dialogSaveOrDownload fileType headerHeight appWidth model =
let
fileTypeString =
case fileType of
Pdf -> "PDF"
Jpeg -> "JPEG"
fileName =
case fileType of
Pdf ->
case model.pdfFile of
Just pdfFile -> pdfFile
Nothing -> ""
Jpeg ->
case model.selectedImage of
Just index ->
case List.head <| List.drop index model.images of
Just value -> value
Nothing -> ""
Nothing -> ""
buttonAttributes =
[ height fill
, Background.color primaryButtonColor
, Border.rounded <| appWidth // 80
, Font.size <| appWidth // 20
, Font.color primaryTextColor
]
in
[ column
[ height fill
, width fill
, spacing <| appWidth // 40
]
[ Input.button
(List.append buttonAttributes [ width fill ])
{ onPress =
case fileType of
Pdf ->
case model.pdfFile of
Just pdfFile -> Just <| SavePdf pdfFile
Nothing -> Just ClearDialog
Jpeg -> Just SaveImage
, label = text <| "Save " ++ fileTypeString ++ " to " ++ storageName
}
, row
[ height fill
, width fill
, spacing <| appWidth // 40
]
[ Input.button
(List.append buttonAttributes [ width fill ])
{ onPress = Nothing
, label =
download
[ height fill
, width fill
]
{ url = "image-cache/" ++ fileName
, label =
el
[ centerX
, centerY
]
<| text <| "Download " ++ fileTypeString
}
}
, Input.button
(List.append buttonAttributes [ width <| px <| appWidth // 4 ])
{ onPress = Just ClearDialog
, label = text "Back"
}
]
]
]
dialogSaved fileType headerHeight appWidth =
let
fileTypeString =
case fileType of
Pdf -> "PDF"
Jpeg -> "JPEG"
in
[ column
[ height fill
, width fill
]
[ el
[ centerX
, centerY
, Font.size <| appWidth // 16
]
<| text <| fileTypeString ++ " has been saved to " ++ storageName ++ "."
, Input.button
[ height <| px <| headerHeight // 3
, width <| px <| appWidth // 5
, Background.color primaryButtonColor
, Border.rounded <| appWidth // 80
, centerX
, alignBottom
, Font.size <| appWidth // 20
, Font.color primaryTextColor
]
{ onPress = Just ClearDialog
, label = text "OK"
}
]
]
globalUI headerHeight appWidth model =
let
buttonAttributes =
[ height fill
, width fill
, Background.color primaryButtonColor
, Border.rounded <| appWidth // 30
, Font.size <| appWidth // 10
, Font.color primaryTextColor
]
in
case model.dialog of
SaveOrDownloadPdf -> dialogSaveOrDownload Pdf headerHeight appWidth model
SaveOrDownloadJpeg -> dialogSaveOrDownload Jpeg headerHeight appWidth model
PdfSaved -> dialogSaved Pdf headerHeight appWidth
JpegSaved -> dialogSaved Jpeg headerHeight appWidth
NoDialog ->
if not model.scannerReady then
[ text "Waiting for scanner..." ]
else
[ Input.button
buttonAttributes
{ onPress = Just ScanImage
, label = text "Scan"
}
, if not <| List.isEmpty model.images then
Input.button
buttonAttributes
{ onPress = Just MakePdf
, label = text "PDF"
}
else none
]
imageUI imageWidth =
let
imageHeight =
(toFloat imageWidth) * 1.376 |> round
buttonSize =
imageWidth // 6
localButtonAttributes =
[ height <| px buttonSize
, width <| px buttonSize
, Background.color secondaryButtonColor
, Border.rounded <| imageWidth // 20
]
makeIcon icon =
html <| icon buttonSize <| RGBA 1 1 1 1
in
column
[ height <| px imageHeight
, width <| px imageWidth
]
[ column
[ height <| px imageWidth
, width <| px imageWidth
, centerY
, padding <| imageWidth // 10
]
[ row
[ height <| px buttonSize
, width fill
]
[ Input.button
( centerX :: localButtonAttributes )
{ onPress = Just ImageUp
, label = makeIcon Ionicon.arrowUpC
}
]
, row
[ height <| px buttonSize
, width fill
, centerY
]
[ Input.button
( alignLeft :: localButtonAttributes )
{ onPress = Just ShowSaveOrDownloadJpegDialog
, label = makeIcon Ionicon.disc
}
, Input.button
( centerX :: localButtonAttributes )
{ onPress = Just RotateImage
, label = makeIcon Ionicon.loop
}
, Input.button
( alignRight :: localButtonAttributes )
{ onPress = Just RemoveImage
, label = makeIcon Ionicon.trashA
}
]
, row
[ height <| px buttonSize
, width fill
, alignBottom
]
[ Input.button
( centerX :: localButtonAttributes )
{ onPress = Just ImageDown
, label = makeIcon Ionicon.arrowDownC
}
]
]
]
renderImages appWidth model =
let
imageWidth =
appWidth - (appWidth // 10)
renderImage index fileName =
let
imageAttributes =
[ width <| px imageWidth
, centerX
, Element.Events.onClick <| SelectImage index
]
in
image
(
case model.selectedImage of
Nothing ->
imageAttributes
Just value ->
if value == index then
(Element.inFront <| imageUI imageWidth) :: imageAttributes
else imageAttributes
)
{ src = "image-cache/" ++ fileName
, description = "scanned image"
}
in
List.indexedMap renderImage model.images
viewPrimary : Model -> Element Msg
viewPrimary model =
let
headerHeight =
model.viewportHeight // 5
appWidth =
if model.viewportWidth <= 500 then
model.viewportWidth
else 500
in
column
[ height fill, width fill ]
[ row
[ height <| px headerHeight
, width fill
, Background.color headerColor
]
[ row
[ height fill
, width <| px appWidth
, centerX
, padding <| appWidth // 40
, spacing <| appWidth // 40
, Font.center
]
<| globalUI headerHeight appWidth model
]
, row
[ height <| px <| model.viewportHeight - headerHeight
, width fill
, scrollbarY
, Background.color mainBackgroundColor
]
[ column
[ height fill
, width <| px appWidth
, centerX
, Font.color primaryTextColor
, paddingXY 0 10
, spacing 20
]
<| renderImages appWidth model
]
]
view : Model -> Html Msg
view model =
let
style =
focusStyle
{ borderColor = Nothing
, backgroundColor = Nothing, shadow = Nothing
}
in
layoutWith { options = [ style ] } [] <| viewPrimary model