elm-video/src/Video/Views.elm

609 lines
19 KiB
Elm

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
}