Cleaning, making generic

This commit is contained in:
Thomas Forgione 2021-06-21 11:05:25 +02:00
parent 51dd1736fd
commit e26971d995
9 changed files with 356 additions and 322 deletions

View File

@ -19,7 +19,7 @@ dev: js/main.js
release: js/main.min.js release: js/main.min.js
js/main.js: src/** js/main.js: src/**
$(ELM) make src/Main.elm --output $(BUILD_DIR)/main.js $(ELM) make src/Examples/Embed.elm --output $(BUILD_DIR)/main.js
js/main.min.js: js/main.tmp.js js/main.min.js: js/main.tmp.js
@$(UGLIFYJS) $(BUILD_DIR)/main.tmp.js --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle > $(BUILD_DIR)/main.min.js @$(UGLIFYJS) $(BUILD_DIR)/main.tmp.js --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle > $(BUILD_DIR)/main.min.js
@ -28,7 +28,7 @@ js/main.tmp.js: src/**
@$(ELM) make src/Main.elm --optimize --output $(BUILD_DIR)/main.tmp.js @$(ELM) make src/Main.elm --optimize --output $(BUILD_DIR)/main.tmp.js
watch: watch:
@$(ELMLIVE) src/Main.elm -p 7000 -d . -- --output $(BUILD_DIR)/main.js @$(ELMLIVE) src/Examples/Embed.elm -p 7000 -d . -- --output $(BUILD_DIR)/main.js
clean: clean:
@rm -rf $(BUILD_DIR)/{main.js,main.min.js} @rm -rf $(BUILD_DIR)/{main.js,main.min.js}

View File

@ -4,11 +4,6 @@
<title>twitch.tforgione.fr</title> <title>twitch.tforgione.fr</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<style>
.filled {
fill: currentColor;
}
</style>
</head> </head>
<body> <body>
<div id="container"></div> <div id="container"></div>

View File

@ -27,16 +27,20 @@ function embed(options) {
options.width = window.innerWidth; options.width = window.innerWidth;
options.height = window.innerHeight; options.height = window.innerHeight;
const app = Elm.Main.init(options); const app = Elm.Examples.Embed.init(options);
setupApp(app);
}
function setupApp(app) {
let hls; let hls;
app.ports.polymnyVideoInit.subscribe(function(arg) { app.ports.polymnyVideoInit.subscribe(function(arg) {
const video = document.getElementById('video'); const video = document.getElementById(arg[0]);
if (Hls.isSupported()) { if (Hls.isSupported()) {
hls = new Hls(); hls = new Hls();
window.hls = hls; window.hls = hls;
hls.loadSource(arg); hls.loadSource(arg[1]);
hls.on(Hls.Events.MANIFEST_PARSED, function(event, data) { hls.on(Hls.Events.MANIFEST_PARSED, function(event, data) {
const availableQualities = hls.levels.map((l) => l.height); const availableQualities = hls.levels.map((l) => l.height);
@ -61,12 +65,12 @@ function embed(options) {
hls.attachMedia(video); hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) { } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = arg; video.src = arg[1];
} }
}); });
app.ports.polymnyVideoPlayPause.subscribe(function() { app.ports.polymnyVideoPlayPause.subscribe(function(arg) {
const video = document.getElementById('video'); const video = document.getElementById(arg);
if (video.paused) { if (video.paused) {
video.play(); video.play();
} else { } else {
@ -75,30 +79,30 @@ function embed(options) {
}); });
app.ports.polymnyVideoSeek.subscribe(function(arg) { app.ports.polymnyVideoSeek.subscribe(function(arg) {
const video = document.getElementById('video'); const video = document.getElementById(arg[0]);
video.currentTime = arg; video.currentTime = arg[1];
}); });
app.ports.polymnyVideoRequestFullscreen.subscribe(function() { app.ports.polymnyVideoRequestFullscreen.subscribe(function(arg) {
document.getElementById('full').requestFullscreen(); document.getElementById(arg + '-full').requestFullscreen();
}); });
app.ports.polymnyVideoExitFullscreen.subscribe(function() { app.ports.polymnyVideoExitFullscreen.subscribe(function(arg) {
document.exitFullscreen(); document.exitFullscreen();
}); });
app.ports.polymnyVideoSetPlaybackRate.subscribe(function(arg) { app.ports.polymnyVideoSetPlaybackRate.subscribe(function(arg) {
const video = document.getElementById('video'); const video = document.getElementById(arg[0]);
video.playbackRate = arg; video.playbackRate = arg[1];
}); });
app.ports.polymnyVideoSetQuality.subscribe(function(arg) { app.ports.polymnyVideoSetQuality.subscribe(function(arg) {
var old = hls.currentLevel; var old = hls.currentLevel;
if (arg.auto) { if (arg[1].auto) {
hls.currentLevel = -1; hls.currentLevel = -1;
} else { } else {
hls.levels.forEach((level, levelIndex) => { hls.levels.forEach((level, levelIndex) => {
if (level.height === arg.height) { if (level.height === arg[1].height) {
hls.currentLevel = levelIndex; hls.currentLevel = levelIndex;
} }
}); });
@ -112,14 +116,14 @@ function embed(options) {
}); });
app.ports.polymnyVideoSetVolume.subscribe(function(arg) { app.ports.polymnyVideoSetVolume.subscribe(function(arg) {
const video = document.getElementById('video'); const video = document.getElementById(arg[0]);
video.volume = arg.volume; video.volume = arg[1].volume;
video.muted = arg.muted; video.muted = arg[1].muted;
}); });
app.ports.polymnyVideoSetSubtitleTrack.subscribe(function(arg) { app.ports.polymnyVideoSetSubtitleTrack.subscribe(function(arg) {
hls.subtitleDisplay = arg !== -1; hls.subtitleDisplay = arg[1] !== -1;
hls.subtitleTrack = arg; hls.subtitleTrack = arg[1];
}); });
} }

View File

@ -1,4 +1,4 @@
module Main exposing (..) module Examples.Embed exposing (..)
import Browser import Browser
import Browser.Events import Browser.Events
@ -7,18 +7,16 @@ import Element.Background as Background
import Element.Border as Border import Element.Border as Border
import Element.Font as Font import Element.Font as Font
import Element.Input as Input import Element.Input as Input
import Events
import Html import Html
import Html.Attributes import Html.Attributes
import Html.Events import Html.Events
import Icons
import Json.Decode as Decode import Json.Decode as Decode
import Quality
import Simple.Animation as Animation exposing (Animation) import Simple.Animation as Animation exposing (Animation)
import Simple.Animation.Animated as Animated import Simple.Animation.Animated as Animated
import Simple.Animation.Property as P import Simple.Animation.Property as P
import Video exposing (Video) import Video exposing (Video)
import Views import Video.Events as Events
import Video.Views as Views
main : Program Decode.Value Model Msg main : Program Decode.Value Model Msg
@ -44,12 +42,6 @@ type alias Model =
} }
type Settings
= All
| Speed
| Quality
type Msg type Msg
= Noop = Noop
| VideoMsg Video.Msg | VideoMsg Video.Msg
@ -70,9 +62,12 @@ init flags =
height = height =
Decode.decodeValue (Decode.field "height" Decode.int) flags Decode.decodeValue (Decode.field "height" Decode.int) flags
|> Result.withDefault 0 |> Result.withDefault 0
( video, cmd ) =
Video.fromUrl url "video"
in in
( { video = Video.fromUrl url, screenSize = ( width, height ) } ( { video = video, screenSize = ( width, height ) }
, Video.init url , Cmd.map VideoMsg cmd
) )

View File

@ -13,13 +13,14 @@ port module Video exposing
) )
import Element exposing (Element) import Element exposing (Element)
import Icons
import Json.Decode as Decode import Json.Decode as Decode
import Quality exposing (Quality) import Video.Icons as Icons
import Video.Quality as Quality exposing (Quality)
type alias Video = type alias Video =
{ url : String { url : String
, id : String
, playing : Bool , playing : Bool
, position : Float , position : Float
, duration : Float , duration : Float
@ -43,30 +44,33 @@ type alias Video =
} }
fromUrl : String -> Video fromUrl : String -> String -> ( Video, Cmd Msg )
fromUrl url = fromUrl url id =
{ url = url ( { url = url
, playing = False , id = id
, position = 0 , playing = False
, duration = 0 , position = 0
, loaded = [] , duration = 0
, volume = 1 , loaded = []
, muted = False , volume = 1
, isFullscreen = False , muted = False
, quality = Nothing , isFullscreen = False
, qualities = [] , quality = Nothing
, showBar = True , qualities = []
, animationFrame = 0 , showBar = True
, size = ( 0, 0 ) , animationFrame = 0
, playbackRate = 1 , size = ( 0, 0 )
, settings = All , playbackRate = 1
, showSettings = False , settings = All
, subtitles = [] , showSettings = False
, subtitleTrack = Nothing , subtitles = []
, showMiniature = Nothing , subtitleTrack = Nothing
, showIcon = Nothing , showMiniature = Nothing
, showIconRequested = Nothing , showIcon = Nothing
} , showIconRequested = Nothing
}
, init id url
)
type Settings type Settings
@ -131,7 +135,7 @@ update msg model =
else else
Just (Icons.play True) Just (Icons.play True)
} }
, playPause , playPause model.id
) )
Seek time -> Seek time ->
@ -143,11 +147,11 @@ update msg model =
else else
Just (Icons.rewind True) Just (Icons.rewind True)
} }
, seek time , seek model.id time
) )
SetPlaybackRate rate -> SetPlaybackRate rate ->
( { model | showSettings = False, settings = All }, setPlaybackRate rate ) ( { model | showSettings = False, settings = All }, setPlaybackRate model.id rate )
ToggleSettings -> ToggleSettings ->
( { model | showSettings = not model.showSettings }, Cmd.none ) ( { model | showSettings = not model.showSettings }, Cmd.none )
@ -156,16 +160,16 @@ update msg model =
( { model | settings = s }, Cmd.none ) ( { model | settings = s }, Cmd.none )
RequestFullscreen -> RequestFullscreen ->
( model, requestFullscreen ) ( model, requestFullscreen model.id )
ExitFullscreen -> ExitFullscreen ->
( model, exitFullscreen ) ( model, exitFullscreen model.id )
SetQuality q -> SetQuality q ->
( { model | showSettings = False, settings = All }, setQuality q ) ( { model | showSettings = False, settings = All }, setQuality model.id q )
SetSubtitleTrack t -> SetSubtitleTrack t ->
( { model | showSettings = False, settings = All }, setSubtitleTrack t ) ( { model | showSettings = False, settings = All }, setSubtitleTrack model.id t )
SetVolume v m -> SetVolume v m ->
( { model ( { model
@ -179,7 +183,7 @@ update msg model =
else else
Just (Icons.volume1 True) Just (Icons.volume1 True)
} }
, setVolume { volume = v, muted = m } , setVolume model.id { volume = v, muted = m }
) )
AnimationFrameDelta delta -> AnimationFrameDelta delta ->
@ -261,76 +265,76 @@ update msg model =
( { model | showMiniature = miniature }, Cmd.none ) ( { model | showMiniature = miniature }, Cmd.none )
port polymnyVideoInit : String -> Cmd msg port polymnyVideoInit : ( String, String ) -> Cmd msg
init : String -> Cmd msg init : String -> String -> Cmd msg
init = init id url =
polymnyVideoInit polymnyVideoInit ( id, url )
port polymnyVideoPlayPause : () -> Cmd msg port polymnyVideoPlayPause : String -> Cmd msg
playPause : Cmd msg playPause : String -> Cmd msg
playPause = playPause =
polymnyVideoPlayPause () polymnyVideoPlayPause
port polymnyVideoSeek : Float -> Cmd msg port polymnyVideoSeek : ( String, Float ) -> Cmd msg
seek : Float -> Cmd msg seek : String -> Float -> Cmd msg
seek = seek id s =
polymnyVideoSeek polymnyVideoSeek ( id, s )
port polymnyVideoRequestFullscreen : () -> Cmd msg port polymnyVideoRequestFullscreen : String -> Cmd msg
requestFullscreen : Cmd msg requestFullscreen : String -> Cmd msg
requestFullscreen = requestFullscreen =
polymnyVideoRequestFullscreen () polymnyVideoRequestFullscreen
port polymnyVideoExitFullscreen : () -> Cmd msg port polymnyVideoExitFullscreen : String -> Cmd msg
exitFullscreen : Cmd msg exitFullscreen : String -> Cmd msg
exitFullscreen = exitFullscreen =
polymnyVideoExitFullscreen () polymnyVideoExitFullscreen
port polymnyVideoSetPlaybackRate : Float -> Cmd msg port polymnyVideoSetPlaybackRate : ( String, Float ) -> Cmd msg
setPlaybackRate : Float -> Cmd msg setPlaybackRate : String -> Float -> Cmd msg
setPlaybackRate = setPlaybackRate id playbackRate =
polymnyVideoSetPlaybackRate polymnyVideoSetPlaybackRate ( id, playbackRate )
port polymnyVideoSetQuality : Quality -> Cmd msg port polymnyVideoSetQuality : ( String, Quality ) -> Cmd msg
setQuality : Quality -> Cmd msg setQuality : String -> Quality -> Cmd msg
setQuality = setQuality id quality =
polymnyVideoSetQuality polymnyVideoSetQuality ( id, quality )
port polymnyVideoSetSubtitleTrack : Int -> Cmd msg port polymnyVideoSetSubtitleTrack : ( String, Int ) -> Cmd msg
setSubtitleTrack : Int -> Cmd msg setSubtitleTrack : String -> Int -> Cmd msg
setSubtitleTrack = setSubtitleTrack id track =
polymnyVideoSetSubtitleTrack polymnyVideoSetSubtitleTrack ( id, track )
port polymnyVideoSetVolume : { volume : Float, muted : Bool } -> Cmd msg port polymnyVideoSetVolume : ( String, { volume : Float, muted : Bool } ) -> Cmd msg
setVolume : { volume : Float, muted : Bool } -> Cmd msg setVolume : String -> { volume : Float, muted : Bool } -> Cmd msg
setVolume = setVolume id volume =
polymnyVideoSetVolume polymnyVideoSetVolume ( id, volume )
port polymnyVideoNowHasQualities : (List Int -> msg) -> Sub msg port polymnyVideoNowHasQualities : (List Int -> msg) -> Sub msg

View File

@ -1,4 +1,4 @@
module Events exposing (player, seekBar, subs, video) module Video.Events exposing (overlay, player, seekBar, subs, video)
import Browser.Events import Browser.Events
import Element import Element
@ -6,8 +6,8 @@ import Html
import Html.Attributes import Html.Attributes
import Html.Events import Html.Events
import Json.Decode as Decode import Json.Decode as Decode
import Quality
import Video exposing (Video) import Video exposing (Video)
import Video.Quality as Quality
subs : Video -> Sub Video.Msg subs : Video -> Sub Video.Msg
@ -51,13 +51,12 @@ player =
List.map Element.htmlAttribute List.map Element.htmlAttribute
[ Html.Events.on "fullscreenchange" decodeFullscreenChange [ Html.Events.on "fullscreenchange" decodeFullscreenChange
, Html.Events.on "mousemove" (Decode.succeed Video.MouseMove) , Html.Events.on "mousemove" (Decode.succeed Video.MouseMove)
, Html.Events.on "click" (Decode.succeed Video.PlayPause)
] ]
video : List (Html.Attribute Video.Msg) video : Video -> List (Html.Attribute Video.Msg)
video = video model =
[ Html.Attributes.id "video" [ Html.Attributes.id model.id
, Html.Events.on "playing" (Decode.succeed Video.NowPlaying) , Html.Events.on "playing" (Decode.succeed Video.NowPlaying)
, Html.Events.on "pause" (Decode.succeed Video.NowPaused) , Html.Events.on "pause" (Decode.succeed Video.NowPaused)
, Html.Events.on "durationchange" decodeDurationChanged , Html.Events.on "durationchange" decodeDurationChanged
@ -69,6 +68,12 @@ video =
] ]
overlay : List (Element.Attribute Video.Msg)
overlay =
[ Element.htmlAttribute (Html.Events.on "click" (Decode.succeed Video.PlayPause))
]
seekBar : Video -> List (Element.Attribute Video.Msg) seekBar : Video -> List (Element.Attribute Video.Msg)
seekBar model = seekBar model =
List.map Element.htmlAttribute List.map Element.htmlAttribute

View File

@ -1,4 +1,4 @@
module Icons exposing (..) module Video.Icons exposing (..)
import Element exposing (Element) import Element exposing (Element)
import Html exposing (Html) import Html exposing (Html)
@ -10,12 +10,14 @@ svgFeatherIcon : String -> List (Svg msg) -> Bool -> Element msg
svgFeatherIcon className lines f = svgFeatherIcon className lines f =
Element.html Element.html
(svg (svg
[ if f then [ class <| "feather feather-" ++ className
class <| "filled feather feather-" ++ className , fill
(if f then
"currentColor"
else else
class <| "feather feather-" ++ className "none"
, fill "none" )
, height "24" , height "24"
, stroke "currentColor" , stroke "currentColor"
, strokeLinecap "round" , strokeLinecap "round"

View File

@ -1,4 +1,4 @@
module Quality exposing (Quality, decode, isSameOption, toString) module Video.Quality exposing (Quality, decode, isSameOption, toString)
import Json.Decode as Decode import Json.Decode as Decode

View File

@ -1,218 +1,38 @@
module Views exposing (..) module Video.Views exposing (..)
import Element exposing (Element) import Element exposing (Element)
import Element.Background as Background import Element.Background as Background
import Element.Border as Border import Element.Border as Border
import Element.Font as Font import Element.Font as Font
import Element.Input as Input import Element.Input as Input
import Events as Events
import Html import Html
import Html.Attributes import Html.Attributes
import Icons
import Quality
import Simple.Animation as Animation exposing (Animation) import Simple.Animation as Animation exposing (Animation)
import Simple.Animation.Animated as Animated import Simple.Animation.Animated as Animated
import Simple.Animation.Property as P import Simple.Animation.Property as P
import Video exposing (Video) 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 : ( Int, Int ) -> Video -> Element Video.Msg
embed screenSize model = embed screenSize model =
let 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)
overlay =
Element.el
[ Element.width Element.fill
, Element.height Element.fill
, Font.color (Element.rgb 1 1 1)
, if not model.playing then
Background.color (Element.rgba 0 0 0 0.5)
else
Background.color (Element.rgba 0 0 0 0)
]
(case ( model.playing, model.showIcon ) of
( False, _ ) ->
Element.el
[ Element.centerX
, Element.centerY
, Element.scale 10
]
(Icons.play True)
( _, Just icon ) ->
animatedEl fadeOutZoom
[ Background.color (Element.rgb 0 0 0)
, Border.rounded 100
, Element.padding 10
, Element.centerX
, Element.centerY
]
icon
_ ->
Element.none
)
bar =
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 ] }
]
[ 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
(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
)
]
[ 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
]
, 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 ]
]
]
]
)
videoAspectRatio = videoAspectRatio =
toFloat (Tuple.first model.size) / toFloat (Tuple.second model.size) toFloat (Tuple.first model.size) / toFloat (Tuple.second model.size)
@ -239,12 +59,12 @@ embed screenSize model =
) )
in in
Element.el Element.el
(Element.inFront overlay (Element.inFront (overlay model)
:: Element.inFront bar :: Element.inFront (menu model)
:: Element.width Element.fill :: Element.width Element.fill
:: Element.height Element.fill :: Element.height Element.fill
:: Background.color (Element.rgb 0 0 0) :: Background.color (Element.rgb 0 0 0)
:: Element.htmlAttribute (Html.Attributes.id "full") :: Element.htmlAttribute (Html.Attributes.id (model.id ++ "-full"))
:: Events.player :: Events.player
) )
(Element.html (Element.html
@ -254,7 +74,7 @@ embed screenSize model =
:: Html.Attributes.height h :: Html.Attributes.height h
:: Html.Attributes.style "top" (String.fromInt y ++ "px") :: Html.Attributes.style "top" (String.fromInt y ++ "px")
:: Html.Attributes.style "left" (String.fromInt x ++ "px") :: Html.Attributes.style "left" (String.fromInt x ++ "px")
:: Events.video :: Events.video model
) )
[] []
) )
@ -432,6 +252,215 @@ settings model =
) )
overlay : Video -> Element Video.Msg
overlay model =
Element.el
(Element.width Element.fill
:: Element.height Element.fill
:: Font.color (Element.rgb 1 1 1)
:: (if not model.playing then
Background.color (Element.rgba 0 0 0 0.5)
else
Background.color (Element.rgba 0 0 0 0)
)
:: Events.overlay
)
(case ( model.playing, model.showIcon ) of
( False, _ ) ->
Element.el
[ Element.centerX
, Element.centerY
, Element.scale 10
]
(Icons.play True)
( _, 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 : Bool -> Element Video.Msg
playPauseButton playing = playPauseButton playing =
let let