{- Copyright 2020 Eike K. & Contributors SPDX-License-Identifier: AGPL-3.0-or-later -} module Comp.UploadForm exposing (Model, Msg, init, reset, update, view) import Api import Api.Model.BasicResult exposing (BasicResult) import Api.Model.ItemUploadMeta import Comp.Dropzone import Comp.FixedDropdown import Comp.Progress import Data.DropdownStyle as DS import Data.Flags exposing (Flags) import Data.Language exposing (Language) import Data.UiSettings exposing (UiSettings) import Dict exposing (Dict) import File exposing (File) import Html exposing (Html, a, div, h2, h3, i, input, label, p, span, text) import Html.Attributes exposing (action, checked, class, classList, href, id, type_) import Html.Events exposing (onCheck, onClick) import Http import Messages.Comp.UploadForm exposing (Texts) import Page exposing (Page(..)) import Set exposing (Set) import Styles import Util.File exposing (makeFileId) import Util.Maybe import Util.Size type alias Model = { incoming : Bool , singleItem : Bool , files : List File , completed : Set String , errored : Set String , loading : Dict String Int , dropzone : Comp.Dropzone.Model , skipDuplicates : Bool , languageModel : Comp.FixedDropdown.Model Language , language : Maybe Language } type Msg = SubmitUpload | SingleUploadResp String (Result Http.Error BasicResult) | GotProgress String Http.Progress | ToggleIncoming | ToggleSingleItem | Clear | DropzoneMsg Comp.Dropzone.Msg | ToggleSkipDuplicates | LanguageMsg (Comp.FixedDropdown.Msg Language) init : Model init = { incoming = True , singleItem = False , files = [] , completed = Set.empty , errored = Set.empty , loading = Dict.empty , dropzone = Comp.Dropzone.init [] , skipDuplicates = True , languageModel = Comp.FixedDropdown.init Data.Language.all , language = Nothing } reset : Msg reset = Clear isLoading : Model -> File -> Bool isLoading model file = Dict.member (makeFileId file) model.loading isCompleted : Model -> File -> Bool isCompleted model file = Set.member (makeFileId file) model.completed isError : Model -> File -> Bool isError model file = Set.member (makeFileId file) model.errored isIdle : Model -> File -> Bool isIdle model file = not (isLoading model file || isCompleted model file || isError model file) uploadAllTracker : String uploadAllTracker = "upload-all" isDone : Model -> Bool isDone model = List.map makeFileId model.files |> List.all (\id -> Set.member id model.completed || Set.member id model.errored) isSuccessAll : Model -> Bool isSuccessAll model = List.map makeFileId model.files |> List.all (\id -> Set.member id model.completed) hasErrors : Model -> Bool hasErrors model = not (Set.isEmpty model.errored) --- Update update : Maybe String -> Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) update sourceId flags msg model = case msg of ToggleIncoming -> ( { model | incoming = not model.incoming }, Cmd.none, Sub.none ) ToggleSingleItem -> ( { model | singleItem = not model.singleItem }, Cmd.none, Sub.none ) ToggleSkipDuplicates -> ( { model | skipDuplicates = not model.skipDuplicates }, Cmd.none, Sub.none ) SubmitUpload -> let emptyMeta = Api.Model.ItemUploadMeta.empty meta = { emptyMeta | multiple = not model.singleItem , skipDuplicates = Just model.skipDuplicates , direction = if model.incoming then Just "incoming" else Just "outgoing" , language = Maybe.map Data.Language.toIso3 model.language } fileids = List.map makeFileId model.files uploads = if model.singleItem then Api.uploadSingle flags sourceId meta uploadAllTracker model.files (SingleUploadResp uploadAllTracker) else Cmd.batch (Api.upload flags sourceId meta model.files SingleUploadResp) tracker = if model.singleItem then Http.track uploadAllTracker (GotProgress uploadAllTracker) else Sub.batch <| List.map (\id -> Http.track id (GotProgress id)) fileids ( cm2, _, _ ) = Comp.Dropzone.update (Comp.Dropzone.setActive False) model.dropzone nowLoading = List.map (\fid -> ( fid, 0 )) fileids |> Dict.fromList in ( { model | loading = nowLoading, dropzone = cm2 }, uploads, tracker ) SingleUploadResp fileid (Ok res) -> let compl = if res.success then setCompleted model fileid else model.completed errs = if not res.success then setErrored model fileid else model.errored load = if fileid == uploadAllTracker then Dict.empty else Dict.remove fileid model.loading in ( { model | completed = compl, errored = errs, loading = load } , Cmd.none , Sub.none ) SingleUploadResp fileid (Err _) -> let errs = setErrored model fileid load = if fileid == uploadAllTracker then Dict.empty else Dict.remove fileid model.loading in ( { model | errored = errs, loading = load }, Cmd.none, Sub.none ) GotProgress fileid progress -> let percent = case progress of Http.Sending p -> Http.fractionSent p |> (*) 100 |> round _ -> 0 newLoading = if model.singleItem then Dict.insert uploadAllTracker percent model.loading else Dict.insert fileid percent model.loading in ( { model | loading = newLoading } , Cmd.none , Sub.none ) Clear -> ( init, Cmd.none, Sub.none ) DropzoneMsg m -> let ( m2, c2, files ) = Comp.Dropzone.update m model.dropzone nextFiles = List.append model.files files in ( { model | files = nextFiles, dropzone = m2 }, Cmd.map DropzoneMsg c2, Sub.none ) LanguageMsg lm -> let ( dm, sel ) = Comp.FixedDropdown.update lm model.languageModel in ( { model | languageModel = dm , language = Util.Maybe.or [ sel, model.language ] } , Cmd.none , Sub.none ) setCompleted : Model -> String -> Set String setCompleted model fileid = if fileid == uploadAllTracker then List.map makeFileId model.files |> Set.fromList else Set.insert fileid model.completed setErrored : Model -> String -> Set String setErrored model fileid = if fileid == uploadAllTracker then List.map makeFileId model.files |> Set.fromList else Set.insert fileid model.errored --- View type alias ViewSettings = { showForm : Bool , sourceId : Maybe String , lightForm : Bool } view : Texts -> ViewSettings -> Flags -> UiSettings -> Model -> Html Msg view texts viewCfg _ _ model = let showForm = viewCfg.sourceId == Nothing && viewCfg.showForm dropzoneCfg = { light = viewCfg.lightForm } in div [ class "mx-auto" ] [ div [ class "px-0 flex flex-col" ] [ if showForm then div [ class "mb-4" ] [ renderForm texts model ] else span [ class "hidden" ] [] , div [ class "py-0" ] [ Html.map DropzoneMsg (Comp.Dropzone.view2 texts.dropzone dropzoneCfg model.dropzone) ] , div [ class "py-4" ] [ a [ class Styles.primaryButton , href "#" , onClick SubmitUpload ] [ text texts.basics.submit ] , a [ class Styles.secondaryButton , class "ml-2" , href "#" , onClick Clear ] [ text texts.reset ] ] ] , renderErrorMsg texts model , renderSuccessMsg texts (Util.Maybe.nonEmpty viewCfg.sourceId) model , renderUploads texts model ] renderForm : Texts -> Model -> Html Msg renderForm texts model = let languageCfg = { display = texts.languageLabel , icon = \_ -> Nothing , style = DS.mainStyleWith "w-40" , selectPlaceholder = texts.basics.selectPlaceholder } in div [ class "row" ] [ Html.form [ action "#" ] [ div [ class "flex flex-col mb-3" ] [ label [ class "inline-flex items-center" ] [ input [ type_ "radio" , checked model.incoming , onCheck (\_ -> ToggleIncoming) , class Styles.radioInput ] [] , span [ class "ml-2" ] [ text texts.basics.incoming ] ] , label [ class "inline-flex items-center" ] [ input [ type_ "radio" , checked (not model.incoming) , onCheck (\_ -> ToggleIncoming) , class Styles.radioInput ] [] , span [ class "ml-2" ] [ text texts.basics.outgoing ] ] ] , div [ class "flex flex-col mb-3" ] [ label [ class "inline-flex items-center" ] [ input [ type_ "checkbox" , checked model.singleItem , onCheck (\_ -> ToggleSingleItem) , class Styles.checkboxInput ] [] , span [ class "ml-2" ] [ text texts.allFilesOneItem ] ] ] , div [ class "flex flex-col mb-3" ] [ label [ class "inline-flex items-center" ] [ input [ type_ "checkbox" , checked model.skipDuplicates , onCheck (\_ -> ToggleSkipDuplicates) , class Styles.checkboxInput ] [] , span [ class "ml-2" ] [ text texts.skipExistingFiles ] ] ] , div [ class "flex flex-col mb-3" ] [ label [ class "inline-flex items-center mb-2" ] [ span [ class "mr-2" ] [ text (texts.language ++ ":") ] , Html.map LanguageMsg (Comp.FixedDropdown.viewStyled2 languageCfg False model.language model.languageModel ) ] , div [ class "text-gray-400 text-xs" ] [ text texts.languageInfo ] ] ] ] renderErrorMsg : Texts -> Model -> Html Msg renderErrorMsg texts model = div [ class "row" , classList [ ( "hidden", not (isDone model && hasErrors model) ) ] ] [ div [ class "mt-4" ] [ div [ class Styles.errorMessage ] [ text texts.uploadErrorMessage ] ] ] renderSuccessMsg : Texts -> Bool -> Model -> Html Msg renderSuccessMsg texts public model = div [ class "row" , classList [ ( "hidden", List.isEmpty model.files || not (isSuccessAll model) ) ] ] [ div [ class "mt-4" ] [ div [ class Styles.successMessage ] [ h3 [ class Styles.header2, class "text-green-800 dark:text-lime-800" ] [ i [ class "fa fa-smile font-thin" ] [] , span [ class "ml-2" ] [ text texts.successBox.allFilesUploaded ] ] , p [ classList [ ( "hidden", public ) ] ] [ text texts.successBox.line1 , a [ class Styles.successMessageLink , Page.href (SearchPage Nothing) ] [ text texts.successBox.itemsPage ] , text texts.successBox.line2 , a [ class Styles.successMessageLink , Page.href QueuePage ] [ text texts.successBox.processingPage ] , text texts.successBox.line3 ] , p [] [ text texts.successBox.resetLine1 , a [ class Styles.successMessageLink , href "#" , onClick Clear ] [ text texts.successBox.reset ] , text texts.successBox.resetLine2 ] ] ] ] renderUploads : Texts -> Model -> Html Msg renderUploads texts model = div [ class "mt-4" , classList [ ( "hidden", List.isEmpty model.files || isSuccessAll model ) ] ] [ h2 [ class Styles.header2 ] [ text texts.selectedFiles , text (" (" ++ (List.length model.files |> String.fromInt) ++ ")") ] , div [] <| if model.singleItem then List.map (renderFileItem model (Just uploadAllTracker)) model.files else List.map (renderFileItem model Nothing) model.files ] getProgress : Model -> File -> Int getProgress model file = let key = if model.singleItem then uploadAllTracker else makeFileId file in Dict.get key model.loading |> Maybe.withDefault 0 renderFileItem : Model -> Maybe String -> File -> Html Msg renderFileItem model _ file = let name = File.name file size = File.size file |> toFloat |> Util.Size.bytesReadable Util.Size.B in div [ class "flex flex-col w-full mb-4" ] [ div [ class "flex flex-row items-center" ] [ div [ class "inline-flex items-center" ] [ i [ classList [ ( "mr-2 text-lg", True ) , ( "fa fa-file font-thin", isIdle model file ) , ( "fa fa-spinner animate-spin ", isLoading model file ) , ( "fa fa-check ", isCompleted model file ) , ( "fa fa-bolt", isError model file ) ] ] [] , div [ class "middle aligned content" ] [ div [ class "header" ] [ text name ] ] ] , div [ class "flex-grow inline-flex justify-end" ] [ text size ] ] , div [ class "h-4" ] [ Comp.Progress.progress2 (getProgress model file) ] ]