-- Copyright © 2023 Dean Lee 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)) -- user interface -- for use with Ionicon 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