diff --git a/Makefile b/Makefile
index 33a9a66..a4e9e0a 100644
--- a/Makefile
+++ b/Makefile
@@ -19,7 +19,7 @@ dev: js/main.js
release: js/main.min.js
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
@$(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
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:
@rm -rf $(BUILD_DIR)/{main.js,main.min.js}
diff --git a/index.html b/index.html
index 75f2efb..a3fd1d6 100644
--- a/index.html
+++ b/index.html
@@ -4,11 +4,6 @@
twitch.tforgione.fr
-
diff --git a/js/ports.js b/js/ports.js
index 4a6b3e6..107fe8f 100644
--- a/js/ports.js
+++ b/js/ports.js
@@ -27,16 +27,20 @@ function embed(options) {
options.width = window.innerWidth;
options.height = window.innerHeight;
- const app = Elm.Main.init(options);
+ const app = Elm.Examples.Embed.init(options);
+ setupApp(app);
+}
+
+function setupApp(app) {
let hls;
app.ports.polymnyVideoInit.subscribe(function(arg) {
- const video = document.getElementById('video');
+ const video = document.getElementById(arg[0]);
if (Hls.isSupported()) {
hls = new Hls();
window.hls = hls;
- hls.loadSource(arg);
+ hls.loadSource(arg[1]);
hls.on(Hls.Events.MANIFEST_PARSED, function(event, data) {
const availableQualities = hls.levels.map((l) => l.height);
@@ -61,12 +65,12 @@ function embed(options) {
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
- video.src = arg;
+ video.src = arg[1];
}
});
- app.ports.polymnyVideoPlayPause.subscribe(function() {
- const video = document.getElementById('video');
+ app.ports.polymnyVideoPlayPause.subscribe(function(arg) {
+ const video = document.getElementById(arg);
if (video.paused) {
video.play();
} else {
@@ -75,30 +79,30 @@ function embed(options) {
});
app.ports.polymnyVideoSeek.subscribe(function(arg) {
- const video = document.getElementById('video');
- video.currentTime = arg;
+ const video = document.getElementById(arg[0]);
+ video.currentTime = arg[1];
});
- app.ports.polymnyVideoRequestFullscreen.subscribe(function() {
- document.getElementById('full').requestFullscreen();
+ app.ports.polymnyVideoRequestFullscreen.subscribe(function(arg) {
+ document.getElementById(arg + '-full').requestFullscreen();
});
- app.ports.polymnyVideoExitFullscreen.subscribe(function() {
+ app.ports.polymnyVideoExitFullscreen.subscribe(function(arg) {
document.exitFullscreen();
});
app.ports.polymnyVideoSetPlaybackRate.subscribe(function(arg) {
- const video = document.getElementById('video');
- video.playbackRate = arg;
+ const video = document.getElementById(arg[0]);
+ video.playbackRate = arg[1];
});
app.ports.polymnyVideoSetQuality.subscribe(function(arg) {
var old = hls.currentLevel;
- if (arg.auto) {
+ if (arg[1].auto) {
hls.currentLevel = -1;
} else {
hls.levels.forEach((level, levelIndex) => {
- if (level.height === arg.height) {
+ if (level.height === arg[1].height) {
hls.currentLevel = levelIndex;
}
});
@@ -112,14 +116,14 @@ function embed(options) {
});
app.ports.polymnyVideoSetVolume.subscribe(function(arg) {
- const video = document.getElementById('video');
- video.volume = arg.volume;
- video.muted = arg.muted;
+ const video = document.getElementById(arg[0]);
+ video.volume = arg[1].volume;
+ video.muted = arg[1].muted;
});
app.ports.polymnyVideoSetSubtitleTrack.subscribe(function(arg) {
- hls.subtitleDisplay = arg !== -1;
- hls.subtitleTrack = arg;
+ hls.subtitleDisplay = arg[1] !== -1;
+ hls.subtitleTrack = arg[1];
});
}
diff --git a/src/Main.elm b/src/Examples/Embed.elm
similarity index 90%
rename from src/Main.elm
rename to src/Examples/Embed.elm
index 29c8c0f..bcc7cfb 100644
--- a/src/Main.elm
+++ b/src/Examples/Embed.elm
@@ -1,4 +1,4 @@
-module Main exposing (..)
+module Examples.Embed exposing (..)
import Browser
import Browser.Events
@@ -7,18 +7,16 @@ import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input
-import Events
import Html
import Html.Attributes
import Html.Events
-import Icons
import Json.Decode as Decode
-import Quality
import Simple.Animation as Animation exposing (Animation)
import Simple.Animation.Animated as Animated
import Simple.Animation.Property as P
import Video exposing (Video)
-import Views
+import Video.Events as Events
+import Video.Views as Views
main : Program Decode.Value Model Msg
@@ -44,12 +42,6 @@ type alias Model =
}
-type Settings
- = All
- | Speed
- | Quality
-
-
type Msg
= Noop
| VideoMsg Video.Msg
@@ -70,9 +62,12 @@ init flags =
height =
Decode.decodeValue (Decode.field "height" Decode.int) flags
|> Result.withDefault 0
+
+ ( video, cmd ) =
+ Video.fromUrl url "video"
in
- ( { video = Video.fromUrl url, screenSize = ( width, height ) }
- , Video.init url
+ ( { video = video, screenSize = ( width, height ) }
+ , Cmd.map VideoMsg cmd
)
diff --git a/src/Video.elm b/src/Video.elm
index 66c21df..197d1ad 100644
--- a/src/Video.elm
+++ b/src/Video.elm
@@ -13,13 +13,14 @@ port module Video exposing
)
import Element exposing (Element)
-import Icons
import Json.Decode as Decode
-import Quality exposing (Quality)
+import Video.Icons as Icons
+import Video.Quality as Quality exposing (Quality)
type alias Video =
{ url : String
+ , id : String
, playing : Bool
, position : Float
, duration : Float
@@ -43,30 +44,33 @@ type alias Video =
}
-fromUrl : String -> Video
-fromUrl url =
- { url = url
- , playing = False
- , position = 0
- , duration = 0
- , loaded = []
- , volume = 1
- , muted = False
- , isFullscreen = False
- , quality = Nothing
- , qualities = []
- , showBar = True
- , animationFrame = 0
- , size = ( 0, 0 )
- , playbackRate = 1
- , settings = All
- , showSettings = False
- , subtitles = []
- , subtitleTrack = Nothing
- , showMiniature = Nothing
- , showIcon = Nothing
- , showIconRequested = Nothing
- }
+fromUrl : String -> String -> ( Video, Cmd Msg )
+fromUrl url id =
+ ( { url = url
+ , id = id
+ , playing = False
+ , position = 0
+ , duration = 0
+ , loaded = []
+ , volume = 1
+ , muted = False
+ , isFullscreen = False
+ , quality = Nothing
+ , qualities = []
+ , showBar = True
+ , animationFrame = 0
+ , size = ( 0, 0 )
+ , playbackRate = 1
+ , settings = All
+ , showSettings = False
+ , subtitles = []
+ , subtitleTrack = Nothing
+ , showMiniature = Nothing
+ , showIcon = Nothing
+ , showIconRequested = Nothing
+ }
+ , init id url
+ )
type Settings
@@ -131,7 +135,7 @@ update msg model =
else
Just (Icons.play True)
}
- , playPause
+ , playPause model.id
)
Seek time ->
@@ -143,11 +147,11 @@ update msg model =
else
Just (Icons.rewind True)
}
- , seek time
+ , seek model.id time
)
SetPlaybackRate rate ->
- ( { model | showSettings = False, settings = All }, setPlaybackRate rate )
+ ( { model | showSettings = False, settings = All }, setPlaybackRate model.id rate )
ToggleSettings ->
( { model | showSettings = not model.showSettings }, Cmd.none )
@@ -156,16 +160,16 @@ update msg model =
( { model | settings = s }, Cmd.none )
RequestFullscreen ->
- ( model, requestFullscreen )
+ ( model, requestFullscreen model.id )
ExitFullscreen ->
- ( model, exitFullscreen )
+ ( model, exitFullscreen model.id )
SetQuality q ->
- ( { model | showSettings = False, settings = All }, setQuality q )
+ ( { model | showSettings = False, settings = All }, setQuality model.id q )
SetSubtitleTrack t ->
- ( { model | showSettings = False, settings = All }, setSubtitleTrack t )
+ ( { model | showSettings = False, settings = All }, setSubtitleTrack model.id t )
SetVolume v m ->
( { model
@@ -179,7 +183,7 @@ update msg model =
else
Just (Icons.volume1 True)
}
- , setVolume { volume = v, muted = m }
+ , setVolume model.id { volume = v, muted = m }
)
AnimationFrameDelta delta ->
@@ -261,76 +265,76 @@ update msg model =
( { model | showMiniature = miniature }, Cmd.none )
-port polymnyVideoInit : String -> Cmd msg
+port polymnyVideoInit : ( String, String ) -> Cmd msg
-init : String -> Cmd msg
-init =
- polymnyVideoInit
+init : String -> String -> Cmd msg
+init id url =
+ polymnyVideoInit ( id, url )
-port polymnyVideoPlayPause : () -> Cmd msg
+port polymnyVideoPlayPause : String -> Cmd msg
-playPause : Cmd msg
+playPause : String -> Cmd msg
playPause =
- polymnyVideoPlayPause ()
+ polymnyVideoPlayPause
-port polymnyVideoSeek : Float -> Cmd msg
+port polymnyVideoSeek : ( String, Float ) -> Cmd msg
-seek : Float -> Cmd msg
-seek =
- polymnyVideoSeek
+seek : String -> Float -> Cmd msg
+seek id s =
+ polymnyVideoSeek ( id, s )
-port polymnyVideoRequestFullscreen : () -> Cmd msg
+port polymnyVideoRequestFullscreen : String -> Cmd msg
-requestFullscreen : Cmd msg
+requestFullscreen : String -> Cmd msg
requestFullscreen =
- polymnyVideoRequestFullscreen ()
+ polymnyVideoRequestFullscreen
-port polymnyVideoExitFullscreen : () -> Cmd msg
+port polymnyVideoExitFullscreen : String -> Cmd msg
-exitFullscreen : Cmd msg
+exitFullscreen : String -> Cmd msg
exitFullscreen =
- polymnyVideoExitFullscreen ()
+ polymnyVideoExitFullscreen
-port polymnyVideoSetPlaybackRate : Float -> Cmd msg
+port polymnyVideoSetPlaybackRate : ( String, Float ) -> Cmd msg
-setPlaybackRate : Float -> Cmd msg
-setPlaybackRate =
- polymnyVideoSetPlaybackRate
+setPlaybackRate : String -> Float -> Cmd msg
+setPlaybackRate id playbackRate =
+ polymnyVideoSetPlaybackRate ( id, playbackRate )
-port polymnyVideoSetQuality : Quality -> Cmd msg
+port polymnyVideoSetQuality : ( String, Quality ) -> Cmd msg
-setQuality : Quality -> Cmd msg
-setQuality =
- polymnyVideoSetQuality
+setQuality : String -> Quality -> Cmd msg
+setQuality id quality =
+ polymnyVideoSetQuality ( id, quality )
-port polymnyVideoSetSubtitleTrack : Int -> Cmd msg
+port polymnyVideoSetSubtitleTrack : ( String, Int ) -> Cmd msg
-setSubtitleTrack : Int -> Cmd msg
-setSubtitleTrack =
- polymnyVideoSetSubtitleTrack
+setSubtitleTrack : String -> Int -> Cmd msg
+setSubtitleTrack id track =
+ 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 =
- polymnyVideoSetVolume
+setVolume : String -> { volume : Float, muted : Bool } -> Cmd msg
+setVolume id volume =
+ polymnyVideoSetVolume ( id, volume )
port polymnyVideoNowHasQualities : (List Int -> msg) -> Sub msg
diff --git a/src/Events.elm b/src/Video/Events.elm
similarity index 96%
rename from src/Events.elm
rename to src/Video/Events.elm
index f567e6a..c1633e6 100644
--- a/src/Events.elm
+++ b/src/Video/Events.elm
@@ -1,4 +1,4 @@
-module Events exposing (player, seekBar, subs, video)
+module Video.Events exposing (overlay, player, seekBar, subs, video)
import Browser.Events
import Element
@@ -6,8 +6,8 @@ import Html
import Html.Attributes
import Html.Events
import Json.Decode as Decode
-import Quality
import Video exposing (Video)
+import Video.Quality as Quality
subs : Video -> Sub Video.Msg
@@ -51,13 +51,12 @@ player =
List.map Element.htmlAttribute
[ Html.Events.on "fullscreenchange" decodeFullscreenChange
, Html.Events.on "mousemove" (Decode.succeed Video.MouseMove)
- , Html.Events.on "click" (Decode.succeed Video.PlayPause)
]
-video : List (Html.Attribute Video.Msg)
-video =
- [ Html.Attributes.id "video"
+video : Video -> List (Html.Attribute Video.Msg)
+video model =
+ [ Html.Attributes.id model.id
, Html.Events.on "playing" (Decode.succeed Video.NowPlaying)
, Html.Events.on "pause" (Decode.succeed Video.NowPaused)
, 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 model =
List.map Element.htmlAttribute
diff --git a/src/Icons.elm b/src/Video/Icons.elm
similarity index 94%
rename from src/Icons.elm
rename to src/Video/Icons.elm
index 728669f..7936956 100644
--- a/src/Icons.elm
+++ b/src/Video/Icons.elm
@@ -1,4 +1,4 @@
-module Icons exposing (..)
+module Video.Icons exposing (..)
import Element exposing (Element)
import Html exposing (Html)
@@ -10,12 +10,14 @@ svgFeatherIcon : String -> List (Svg msg) -> Bool -> Element msg
svgFeatherIcon className lines f =
Element.html
(svg
- [ if f then
- class <| "filled feather feather-" ++ className
+ [ class <| "feather feather-" ++ className
+ , fill
+ (if f then
+ "currentColor"
- else
- class <| "feather feather-" ++ className
- , fill "none"
+ else
+ "none"
+ )
, height "24"
, stroke "currentColor"
, strokeLinecap "round"
diff --git a/src/Quality.elm b/src/Video/Quality.elm
similarity index 92%
rename from src/Quality.elm
rename to src/Video/Quality.elm
index e78f878..3c4d17d 100644
--- a/src/Quality.elm
+++ b/src/Video/Quality.elm
@@ -1,4 +1,4 @@
-module Quality exposing (Quality, decode, isSameOption, toString)
+module Video.Quality exposing (Quality, decode, isSameOption, toString)
import Json.Decode as Decode
diff --git a/src/Views.elm b/src/Video/Views.elm
similarity index 64%
rename from src/Views.elm
rename to src/Video/Views.elm
index 88d1b17..6f41cfe 100644
--- a/src/Views.elm
+++ b/src/Video/Views.elm
@@ -1,218 +1,38 @@
-module Views exposing (..)
+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 Events as Events
import Html
import Html.Attributes
-import Icons
-import Quality
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
- 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 =
toFloat (Tuple.first model.size) / toFloat (Tuple.second model.size)
@@ -239,12 +59,12 @@ embed screenSize model =
)
in
Element.el
- (Element.inFront overlay
- :: Element.inFront bar
+ (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 "full")
+ :: Element.htmlAttribute (Html.Attributes.id (model.id ++ "-full"))
:: Events.player
)
(Element.html
@@ -254,7 +74,7 @@ embed screenSize model =
:: Html.Attributes.height h
:: Html.Attributes.style "top" (String.fromInt y ++ "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 playing =
let