module Video.Views exposing (..) import Element exposing (Element) import Element.Background as Background import Element.Border as Border import Element.Font as Font import Element.Input as Input import Html import Html.Attributes import Simple.Animation as Animation exposing (Animation) import Simple.Animation.Animated as Animated import Simple.Animation.Property as P import Video exposing (Video) import Video.Events as Events import Video.Icons as Icons import Video.Quality as Quality view : Video -> Element Video.Msg view model = Element.el (Element.inFront (overlay model) :: Element.inFront (menu model) :: Element.width Element.fill :: Element.height Element.fill :: Background.color (Element.rgb 0 0 0) :: Element.htmlAttribute (Html.Attributes.id (model.id ++ "-full")) :: Events.player ) (Element.html (Html.video (Html.Attributes.class "wf" :: Events.video model) [])) embed : ( Int, Int ) -> Video -> Element Video.Msg embed screenSize model = let videoAspectRatio = toFloat (Tuple.first model.size) / toFloat (Tuple.second model.size) screenAspectRatio = toFloat (Tuple.first screenSize) / toFloat (Tuple.second screenSize) ( ( x, y ), ( w, h ) ) = if videoAspectRatio > screenAspectRatio then let videoHeight = Tuple.first screenSize * Tuple.second model.size // Tuple.first model.size in ( ( 0, (Tuple.second screenSize - videoHeight) // 2 ) , ( Tuple.first screenSize, videoHeight ) ) else let videoWidth = Tuple.second screenSize * Tuple.first model.size // Tuple.second model.size in ( ( (Tuple.first screenSize - videoWidth) // 2, 0 ) , ( videoWidth, Tuple.second screenSize ) ) in Element.el (Element.inFront (overlay model) :: Element.inFront (menu model) :: Element.width Element.fill :: Element.height Element.fill :: Background.color (Element.rgb 0 0 0) :: Element.htmlAttribute (Html.Attributes.id (model.id ++ "-full")) :: Events.player ) (Element.html (Html.video (Html.Attributes.style "position" "absolute" :: Html.Attributes.width w :: Html.Attributes.height h :: Html.Attributes.style "top" (String.fromInt y ++ "px") :: Html.Attributes.style "left" (String.fromInt x ++ "px") :: Events.video model ) [] ) ) settings : Video -> Element Video.Msg settings model = let makeMenuButton : Video.Settings -> Element Video.Msg -> Element Video.Msg -> Element Video.Msg makeMenuButton s key value = Input.button [ Element.width Element.fill, Element.paddingXY 0 10 ] { label = Element.row [ Element.width Element.fill, Element.spacing 20 ] [ Element.el [ Font.bold, Element.alignLeft ] key , Element.el [ Element.alignRight ] value ] , onPress = Just (Video.SetSettings s) } speedButton = makeMenuButton Video.Speed (Element.text "Speed") (Element.text ("x" ++ String.fromFloat model.playbackRate)) qualityButton = case model.quality of Just q -> makeMenuButton Video.Quality (Element.text "Quality") (Element.text (Quality.toString q)) _ -> Element.none subtitlesButton = case ( model.subtitleTrack, model.subtitles ) of ( Just t, _ :: _ ) -> makeMenuButton Video.Subtitles (Element.text "Subtitles") (Element.text t.name) ( _, _ :: _ ) -> makeMenuButton Video.Subtitles (Element.text "Subtitles") (Element.text "Disabled") _ -> Element.none returnButton = Input.button [ Element.width Element.fill , Element.paddingXY 0 10 , Border.widthEach { bottom = 1 , top = 0 , left = 0 , right = 0 } , Border.color (Element.rgba 0.5 0.5 0.5 0.75) ] { label = Element.text "Return" , onPress = Just (Video.SetSettings Video.All) } speedOptions = [ 0.5, 0.75, 1, 1.5, 2 ] |> List.map (\x -> Input.button [ Element.width Element.fill, Element.paddingXY 0 10 ] { label = Element.row [ Element.width Element.fill ] [ if x == model.playbackRate then Icons.check False else Element.el [ Font.color (Element.rgba 0 0 0 0) ] (Icons.check False) , Element.el [ Element.paddingEach { left = 10 , right = 0 , top = 0 , bottom = 0 } ] (Element.text ("x" ++ String.fromFloat x)) ] , onPress = Just (Video.SetPlaybackRate x) } ) |> (\x -> returnButton :: x) qualityOptions = model.qualities |> List.map (\x -> Input.button [ Element.width Element.fill, Element.paddingXY 0 10 ] { label = Element.row [ Element.width Element.fill ] [ if Quality.isSameOption (Just { auto = False, height = x }) model.quality then Icons.check False else Element.el [ Font.color (Element.rgba 0 0 0 0) ] (Icons.check False) , Element.el [ Element.paddingEach { left = 10 , right = 0 , top = 0 , bottom = 0 } ] (Element.text (Quality.toString { auto = False, height = x })) ] , onPress = Just (Video.SetQuality { auto = x == 0, height = x }) } ) |> (\x -> returnButton :: x) subtitleOptions = model.subtitles |> List.indexedMap (\i x -> ( i, Just x )) |> (\x -> ( -1, Nothing ) :: x) |> List.map (\( i, x ) -> Input.button [ Element.width Element.fill, Element.paddingXY 0 10 ] { label = Element.row [ Element.width Element.fill ] [ if Maybe.map .name model.subtitleTrack == Maybe.map .name x then Icons.check False else Element.el [ Font.color (Element.rgba 0 0 0 0) ] (Icons.check False) , Element.el [ Element.paddingEach { left = 10 , right = 0 , top = 0 , bottom = 0 } ] (Element.text (Maybe.withDefault "Disabled" (Maybe.map .name x))) ] , onPress = Just (Video.SetSubtitleTrack i) } ) |> (\x -> returnButton :: x) buttons = case model.settings of Video.All -> [ speedButton, qualityButton, subtitlesButton ] Video.Speed -> speedOptions Video.Quality -> qualityOptions Video.Subtitles -> subtitleOptions in animatedEl (if model.showSettings then fadeIn else fadeOut ) [ Element.padding 10 , Element.width Element.fill , Element.height Element.fill , Element.moveDown 20 ] (Element.column [ Background.color (Element.rgba 0.2 0.2 0.2 0.75) , Element.alignRight , Element.paddingXY 20 10 , Border.rounded 10 ] buttons ) overlay : Video -> Element Video.Msg overlay model = Element.el (Element.width Element.fill :: Element.height Element.fill :: Font.color (Element.rgb 1 1 1) :: Events.overlay ) (case model.showIcon of Just icon -> animatedEl fadeOutZoom [ Background.color (Element.rgb 0 0 0) , Border.rounded 100 , Element.padding 10 , Element.centerX , Element.centerY ] icon _ -> Element.none ) menu : Video -> Element Video.Msg menu model = animatedEl (if model.animationFrame < 3000 then fadeIn else fadeOut ) [ Element.width Element.fill, Element.alignBottom ] (Element.column [ Element.width Element.fill , Element.alignBottom , Font.color (Element.rgba 1 1 1 0.85) ] [ settings model , Element.column [ Element.width Element.fill , Element.padding 10 , Background.gradient { angle = 0, steps = [ Element.rgba 0 0 0 0.75, Element.rgba 0 0 0 0 ] } ] [ seekbar model , Element.row [ Element.spacing 10, Element.width Element.fill ] [ playPauseButton model.playing , volumeButton model.volume model.muted , Element.el [ Element.moveDown 2.5 ] (Element.text (formatTime model.position ++ " / " ++ formatTime model.duration)) , Element.row [ Element.spacing 10, Element.alignRight ] [ settingsButton, fullscreenButton model.isFullscreen ] ] ] ] ) seekbar : Video -> Element Video.Msg seekbar model = let seen = round (model.position * 1000) loaded = List.filter (\( start, end ) -> start < model.position) model.loaded loadedToShow = every model.duration loaded showRange : ( Float, Float, Bool ) -> Element msg showRange ( start, end, isLoaded ) = let portion = round (1000 * (end - start)) in Element.el [ Element.width (Element.fillPortion portion) , Element.height Element.fill , if isLoaded then Background.color (Element.rgba 1 1 1 0.5) else Background.color (Element.rgba 1 1 1 0) ] Element.none loadedElement = Element.row [ Element.width Element.fill , Element.height (Element.px 5) , Element.centerY , Border.rounded 5 ] (List.map showRange loadedToShow) remaining = round ((model.duration - model.position) * 1000) in Element.row [ Element.width Element.fill , Element.height (Element.px 30) , Border.rounded 5 , Element.behindContent (Element.el [ Background.color (Element.rgba 1 1 1 0.25) , Element.width Element.fill , Element.height (Element.px 5) , Element.centerY , Border.rounded 5 ] Element.none ) , Element.behindContent loadedElement , Element.inFront (Element.el (Element.width Element.fill :: Element.height Element.fill :: Element.pointer :: Events.seekBar model ) Element.none ) , Element.above (miniature model) ] [ Element.el [ Background.color (Element.rgba 1 0 0 0.75) , Element.width (Element.fillPortion seen) , Element.height Element.fill , Border.roundEach { topLeft = 5, topRight = 0, bottomLeft = 5, bottomRight = 0 } , Element.height (Element.px 5) , Element.centerY ] Element.none , Element.el [ Element.width (Element.fillPortion remaining) ] Element.none ] miniature : Video -> Element Video.Msg miniature model = case model.showMiniature of Just ( position, size ) -> let relativePosition = toFloat position / toFloat size percentage = String.fromFloat (relativePosition * 100) ++ "%" miniatureId = round (relativePosition * 100) miniatureIdString = "miniature-" ++ String.padLeft 3 '0' (String.fromInt miniatureId) ++ ".png" miniatureUrl = model.url |> String.split "/" |> List.reverse |> List.drop 1 |> (\list -> miniatureIdString :: list) |> List.reverse |> String.join "/" rightPosition = (position - 180 - 6) |> max 0 |> min (size - 360 - 28) |> toFloat in Element.column [ Element.moveRight rightPosition , Element.spacing 10 ] [ Element.image [ Border.color (Element.rgb 1 1 1) , Border.width 2 ] { src = miniatureUrl, description = "miniature" } , Element.el [ Element.centerX , Font.shadow { offset = ( 0, 0 ) , blur = 4 , color = Element.rgb 0 0 0 } ] (Element.text (formatTime (relativePosition * model.duration))) ] _ -> Element.none playPauseButton : Bool -> Element Video.Msg playPauseButton playing = let icon = if playing then Icons.pause True else Icons.play True in Input.button [] { label = icon , onPress = Just Video.PlayPause } fullscreenButton : Bool -> Element Video.Msg fullscreenButton isFullscreen = Input.button [] (if isFullscreen then { label = Icons.minimize False , onPress = Just Video.ExitFullscreen } else { label = Icons.maximize False , onPress = Just Video.RequestFullscreen } ) volumeButton : Float -> Bool -> Element Video.Msg volumeButton volume muted = let icon = if muted then Icons.volumeX else if volume < 0.3 then Icons.volume else if volume < 0.6 then Icons.volume1 else Icons.volume2 in Input.button [] { label = icon True , onPress = Just (Video.SetVolume volume (not muted)) } settingsButton : Element Video.Msg settingsButton = Input.button [] { label = Icons.settings False , onPress = Just Video.ToggleSettings } every : Float -> List ( Float, Float ) -> List ( Float, Float, Bool ) every duration input = everyAux duration 0.0 [] input |> List.reverse |> List.filter (\( x, y, _ ) -> x /= y) everyAux : Float -> Float -> List ( Float, Float, Bool ) -> List ( Float, Float ) -> List ( Float, Float, Bool ) everyAux duration currentTime currentState input = case input of [] -> ( currentTime, duration, False ) :: currentState [ ( start, end ) ] -> ( end, duration, False ) :: ( start, end, True ) :: ( currentTime, start, False ) :: currentState ( start, end ) :: t -> everyAux duration end (( start, end, True ) :: ( currentTime, start, False ) :: currentState) t formatTime : Float -> String formatTime s = let seconds = round s minutes = seconds // 60 |> modBy 60 hours = seconds // 3600 secs = modBy 60 seconds secsString = if secs < 10 then "0" ++ String.fromInt secs else String.fromInt secs minutesString = if minutes < 10 && hours > 0 then "0" ++ String.fromInt minutes else String.fromInt minutes hoursString = if hours == 0 then "" else String.fromInt hours ++ ":" in hoursString ++ minutesString ++ ":" ++ secsString fadeIn : Animation fadeIn = Animation.fromTo { duration = 500 , options = [] } [ P.opacity 0 ] [ P.opacity 1 ] fadeOut : Animation fadeOut = Animation.fromTo { duration = 500 , options = [] } [ P.opacity 1 ] [ P.opacity 0 ] fadeOutZoom : Animation fadeOutZoom = Animation.fromTo { duration = 500 , options = [] } [ P.opacity 1, P.scale 1 ] [ P.opacity 0, P.scale 5 ] animatedEl : Animation -> List (Element.Attribute msg) -> Element msg -> Element msg animatedEl = animatedUi Element.el animatedUi = Animated.ui { behindContent = Element.behindContent , htmlAttribute = Element.htmlAttribute , html = Element.html }