From e26971d995e3863214f8bbc93de1243064d9aed0 Mon Sep 17 00:00:00 2001 From: Thomas Forgione Date: Mon, 21 Jun 2021 11:05:25 +0200 Subject: [PATCH] Cleaning, making generic --- Makefile | 4 +- index.html | 5 - js/ports.js | 44 +-- src/{Main.elm => Examples/Embed.elm} | 21 +- src/Video.elm | 138 ++++----- src/{ => Video}/Events.elm | 17 +- src/{ => Video}/Icons.elm | 14 +- src/{ => Video}/Quality.elm | 2 +- src/{ => Video}/Views.elm | 433 ++++++++++++++------------- 9 files changed, 356 insertions(+), 322 deletions(-) rename src/{Main.elm => Examples/Embed.elm} (90%) rename src/{ => Video}/Events.elm (96%) rename src/{ => Video}/Icons.elm (94%) rename src/{ => Video}/Quality.elm (92%) rename src/{ => Video}/Views.elm (64%) 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