Started cleaning
This commit is contained in:
		
							parent
							
								
									37a8253387
								
							
						
					
					
						commit
						3193f3c382
					
				
							
								
								
									
										22
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								index.html
									
									
									
									
									
								
							| @ -42,7 +42,7 @@ | ||||
| 
 | ||||
|             let hls; | ||||
| 
 | ||||
|             app.ports.initVideo.subscribe(function(arg) { | ||||
|             app.ports.polymnyVideoInit.subscribe(function(arg) { | ||||
|                 const video = document.getElementById('video'); | ||||
|                 if (Hls.isSupported()) { | ||||
|                     hls = new Hls(); | ||||
| @ -52,11 +52,11 @@ | ||||
|                         // Transform available levels into an array of integers (height values). | ||||
|                         const availableQualities = hls.levels.map((l) => l.height); | ||||
|                         availableQualities.unshift(0); | ||||
|                         app.ports.nowHasQualities.send(availableQualities); | ||||
|                         app.ports.polymnyVideoNowHasQualities.send(availableQualities); | ||||
|                     }); | ||||
| 
 | ||||
|                     hls.on(Hls.Events.LEVEL_SWITCHED, function (event, data) { | ||||
|                         app.ports.nowHasQuality.send({ | ||||
|                         app.ports.polymnyVideoNowHasQuality.send({ | ||||
|                             auto: hls.autoLevelEnabled, | ||||
|                             height: hls.levels[data.level].height | ||||
|                         }); | ||||
| @ -68,7 +68,7 @@ | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             app.ports.playPause.subscribe(function() { | ||||
|             app.ports.polymnyVideoPlayPause.subscribe(function() { | ||||
|                 const video = document.getElementById('video'); | ||||
|                 if (video.paused) { | ||||
|                     video.play(); | ||||
| @ -77,26 +77,26 @@ | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             app.ports.seek.subscribe(function(arg) { | ||||
|             app.ports.polymnyVideoSeek.subscribe(function(arg) { | ||||
|                 const video = document.getElementById('video'); | ||||
|                 console.log(arg); | ||||
|                 video.currentTime = arg; | ||||
|             }); | ||||
| 
 | ||||
|             app.ports.requestFullscreen.subscribe(function() { | ||||
|             app.ports.polymnyVideoRequestFullscreen.subscribe(function() { | ||||
|                 document.getElementById('full').requestFullscreen(); | ||||
|             }); | ||||
| 
 | ||||
|             app.ports.exitFullscreen.subscribe(function() { | ||||
|             app.ports.polymnyVideoExitFullscreen.subscribe(function() { | ||||
|                 document.exitFullscreen(); | ||||
|             }); | ||||
| 
 | ||||
|             app.ports.setPlaybackRate.subscribe(function(arg) { | ||||
|             app.ports.polymnyVideoSetPlaybackRate.subscribe(function(arg) { | ||||
|                 const video = document.getElementById('video'); | ||||
|                 video.playbackRate = arg; | ||||
|             }); | ||||
| 
 | ||||
|             app.ports.setQuality.subscribe(function(arg) { | ||||
|             app.ports.polymnyVideoSetQuality.subscribe(function(arg) { | ||||
|                 var old = hls.currentLevel; | ||||
|                 if (arg.auto) { | ||||
|                     hls.currentLevel = -1; | ||||
| @ -108,14 +108,14 @@ | ||||
|                     }); | ||||
|                 } | ||||
|                 if (old === hls.currentLevel) { | ||||
|                     app.ports.nowHasQuality.send({ | ||||
|                     app.ports.polymnyVideoNowHasQuality.send({ | ||||
|                         auto: hls.autoLevelEnabled, | ||||
|                         height: hls.currentLevel === -1 ? 0 : hls.levels[hls.currentLevel].height | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             app.ports.setVolume.subscribe(function(arg) { | ||||
|             app.ports.polymnyVideoSetVolume.subscribe(function(arg) { | ||||
|                 const video = document.getElementById('video'); | ||||
|                 video.volume = arg.volume; | ||||
|                 video.muted = arg.muted; | ||||
|  | ||||
							
								
								
									
										190
									
								
								src/Events.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/Events.elm
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,190 @@ | ||||
| module Events exposing (player, seekBar, subs, video) | ||||
| 
 | ||||
| import Browser.Events | ||||
| import DOM as Dom | ||||
| import Element | ||||
| import Html | ||||
| import Html.Attributes | ||||
| import Html.Events | ||||
| import Json.Decode as Decode | ||||
| import Quality | ||||
| import Video exposing (Video) | ||||
| 
 | ||||
| 
 | ||||
| subs : Video -> Sub Video.Msg | ||||
| subs model = | ||||
|     Sub.batch | ||||
|         [ Video.nowHasQualities Video.NowHasQualities | ||||
|         , Video.nowHasQuality | ||||
|             (\x -> | ||||
|                 case Decode.decodeValue Quality.decode x of | ||||
|                     Ok s -> | ||||
|                         Video.NowHasQuality s | ||||
| 
 | ||||
|                     _ -> | ||||
|                         Video.Noop | ||||
|             ) | ||||
|         , Browser.Events.onAnimationFrameDelta Video.AnimationFrameDelta | ||||
|         , Browser.Events.onKeyDown (decodeKeyDown model) | ||||
|         ] | ||||
| 
 | ||||
| 
 | ||||
| player : List (Element.Attribute Video.Msg) | ||||
| player = | ||||
|     List.map Element.htmlAttribute | ||||
|         [ Html.Events.on "fullscreenchange" decodeFullscreenChange | ||||
|         , Html.Events.on "mousemove" (Decode.succeed Video.MouseMove) | ||||
|         ] | ||||
| 
 | ||||
| 
 | ||||
| video : List (Html.Attribute Video.Msg) | ||||
| video = | ||||
|     [ Html.Attributes.id "video" | ||||
|     , Html.Events.on "playing" (Decode.succeed Video.NowPlaying) | ||||
|     , Html.Events.on "pause" (Decode.succeed Video.NowPaused) | ||||
|     , Html.Events.on "durationchange" decodeDurationChanged | ||||
|     , Html.Events.on "timeupdate" decodePosition | ||||
|     , Html.Events.on "volumechange" decodeVolumeChange | ||||
|     , Html.Events.on "progress" decodeProgress | ||||
|     , Html.Events.on "resize" decodeVideoResize | ||||
|     , Html.Events.on "ratechange" decodePlaybackRateChange | ||||
|     ] | ||||
| 
 | ||||
| 
 | ||||
| seekBar : Video -> List (Element.Attribute Video.Msg) | ||||
| seekBar model = | ||||
|     List.map Element.htmlAttribute | ||||
|         [ Html.Events.on "click" (decodeSeek model) | ||||
|         ] | ||||
| 
 | ||||
| 
 | ||||
| decodeDurationChanged : Decode.Decoder Video.Msg | ||||
| decodeDurationChanged = | ||||
|     Dom.target <| | ||||
|         Decode.map Video.NowHasDuration | ||||
|             (Decode.field "duration" Decode.float) | ||||
| 
 | ||||
| 
 | ||||
| decodePosition : Decode.Decoder Video.Msg | ||||
| decodePosition = | ||||
|     Dom.target <| | ||||
|         Decode.map Video.NowAtPosition | ||||
|             (Decode.field "currentTime" Decode.float) | ||||
| 
 | ||||
| 
 | ||||
| decodeVolumeChange : Decode.Decoder Video.Msg | ||||
| decodeVolumeChange = | ||||
|     Dom.target <| | ||||
|         Decode.map2 Video.NowAtVolume | ||||
|             (Decode.field "volume" Decode.float) | ||||
|             (Decode.field "muted" Decode.bool) | ||||
| 
 | ||||
| 
 | ||||
| decodeSeek : Video -> Decode.Decoder Video.Msg | ||||
| decodeSeek model = | ||||
|     Decode.map2 (\x y -> Video.Seek (toFloat x / toFloat y * model.duration)) | ||||
|         (Decode.field "layerX" Decode.int) | ||||
|         (Dom.target <| Decode.field "offsetWidth" Decode.int) | ||||
| 
 | ||||
| 
 | ||||
| decodeProgress : Decode.Decoder Video.Msg | ||||
| decodeProgress = | ||||
|     decodeTimeRanges | ||||
|         |> Decode.field "asArray" | ||||
|         |> Decode.field "buffered" | ||||
|         |> Dom.target | ||||
|         |> Decode.map Video.NowLoaded | ||||
| 
 | ||||
| 
 | ||||
| decodeTimeRanges : Decode.Decoder (List ( Float, Float )) | ||||
| decodeTimeRanges = | ||||
|     Decode.list decodeTimeRange | ||||
| 
 | ||||
| 
 | ||||
| decodeTimeRange : Decode.Decoder ( Float, Float ) | ||||
| decodeTimeRange = | ||||
|     Decode.map2 Tuple.pair | ||||
|         (Decode.field "start" Decode.float) | ||||
|         (Decode.field "end" Decode.float) | ||||
| 
 | ||||
| 
 | ||||
| decodeFullscreenChange : Decode.Decoder Video.Msg | ||||
| decodeFullscreenChange = | ||||
|     Decode.value | ||||
|         |> Decode.nullable | ||||
|         |> Decode.field "fullscreenElement" | ||||
|         |> Decode.field "document" | ||||
|         |> Dom.target | ||||
|         |> Decode.map (\x -> Video.NowIsFullscreen (x /= Nothing)) | ||||
| 
 | ||||
| 
 | ||||
| decodeVideoResize : Decode.Decoder Video.Msg | ||||
| decodeVideoResize = | ||||
|     Dom.target <| | ||||
|         Decode.map2 (\x y -> Video.NowHasSize ( x, y )) | ||||
|             (Decode.field "videoWidth" Decode.int) | ||||
|             (Decode.field "videoHeight" Decode.int) | ||||
| 
 | ||||
| 
 | ||||
| decodePlaybackRateChange : Decode.Decoder Video.Msg | ||||
| decodePlaybackRateChange = | ||||
|     Dom.target <| | ||||
|         Decode.map Video.NowHasPlaybackRate | ||||
|             (Decode.field "playbackRate" Decode.float) | ||||
| 
 | ||||
| 
 | ||||
| decodeKeyDown : Video -> Decode.Decoder Video.Msg | ||||
| decodeKeyDown model = | ||||
|     Decode.field "keyCode" Decode.int | ||||
|         |> Decode.andThen | ||||
|             (\x -> | ||||
|                 case x of | ||||
|                     -- Enter key | ||||
|                     32 -> | ||||
|                         Decode.succeed Video.PlayPause | ||||
| 
 | ||||
|                     -- J key | ||||
|                     74 -> | ||||
|                         Decode.succeed (Video.Seek (max 0 (model.position - 10))) | ||||
| 
 | ||||
|                     -- L key | ||||
|                     76 -> | ||||
|                         Decode.succeed (Video.Seek (min model.duration (model.position + 10))) | ||||
| 
 | ||||
|                     -- K key | ||||
|                     75 -> | ||||
|                         Decode.succeed Video.PlayPause | ||||
| 
 | ||||
|                     -- Left arrow | ||||
|                     37 -> | ||||
|                         Decode.succeed (Video.Seek (max 0 (model.position - 5))) | ||||
| 
 | ||||
|                     -- Right arrow | ||||
|                     39 -> | ||||
|                         Decode.succeed (Video.Seek (min model.duration (model.position + 5))) | ||||
| 
 | ||||
|                     -- Down arrow | ||||
|                     40 -> | ||||
|                         Decode.succeed (Video.SetVolume (max 0 (model.volume - 0.1)) model.muted) | ||||
| 
 | ||||
|                     -- Top arrow | ||||
|                     38 -> | ||||
|                         Decode.succeed (Video.SetVolume (min 1 (model.volume + 0.1)) model.muted) | ||||
| 
 | ||||
|                     -- M key | ||||
|                     77 -> | ||||
|                         Decode.succeed (Video.SetVolume model.volume (not model.muted)) | ||||
| 
 | ||||
|                     -- F key | ||||
|                     70 -> | ||||
|                         Decode.succeed | ||||
|                             (if model.isFullscreen then | ||||
|                                 Video.ExitFullscreen | ||||
| 
 | ||||
|                              else | ||||
|                                 Video.RequestFullscreen | ||||
|                             ) | ||||
| 
 | ||||
|                     _ -> | ||||
|                         Decode.fail ("no shortcut for code " ++ String.fromInt x) | ||||
|             ) | ||||
							
								
								
									
										791
									
								
								src/Main.elm
									
									
									
									
									
								
							
							
						
						
									
										791
									
								
								src/Main.elm
									
									
									
									
									
								
							| @ -1,4 +1,4 @@ | ||||
| port module Main exposing (..) | ||||
| module Main exposing (..) | ||||
| 
 | ||||
| import Browser | ||||
| import Browser.Events | ||||
| @ -8,6 +8,7 @@ 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 | ||||
| @ -17,6 +18,8 @@ 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 | ||||
| 
 | ||||
| 
 | ||||
| main : Program Decode.Value Model Msg | ||||
| @ -28,19 +31,8 @@ main = | ||||
|         , subscriptions = | ||||
|             \model -> | ||||
|                 Sub.batch | ||||
|                     [ nowHasQualities NowHasQualities | ||||
|                     , nowHasQuality | ||||
|                         (\x -> | ||||
|                             case Decode.decodeValue Quality.decode x of | ||||
|                                 Ok s -> | ||||
|                                     NowHasQuality s | ||||
| 
 | ||||
|                                 _ -> | ||||
|                                     Noop | ||||
|                         ) | ||||
|                     , Browser.Events.onAnimationFrameDelta AnimationFrameDelta | ||||
|                     , Browser.Events.onResize (\x y -> NowHasWindowSize ( x, y )) | ||||
|                     , Browser.Events.onKeyDown (decodeKeyDown model) | ||||
|                     [ Events.subs model.video |> Sub.map VideoMsg | ||||
|                     , Browser.Events.onResize (\x y -> NowHasScreenSize ( x, y )) | ||||
|                     ] | ||||
|         , onUrlChange = \_ -> Noop | ||||
|         , onUrlRequest = \_ -> Noop | ||||
| @ -48,23 +40,8 @@ main = | ||||
| 
 | ||||
| 
 | ||||
| type alias Model = | ||||
|     { url : String | ||||
|     , playing : Bool | ||||
|     , position : Float | ||||
|     , duration : Float | ||||
|     , loaded : List ( Float, Float ) | ||||
|     , volume : Float | ||||
|     , muted : Bool | ||||
|     , isFullscreen : Bool | ||||
|     , quality : Maybe Quality.Quality | ||||
|     , qualities : List Int | ||||
|     , showBar : Bool | ||||
|     , animationFrame : Float | ||||
|     , videoSize : ( Int, Int ) | ||||
|     { video : Video | ||||
|     , screenSize : ( Int, Int ) | ||||
|     , playbackRate : Float | ||||
|     , settings : Settings | ||||
|     , showSettings : Bool | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @ -76,29 +53,8 @@ type Settings | ||||
| 
 | ||||
| type Msg | ||||
|     = Noop | ||||
|     | PlayPause | ||||
|     | Seek Float | ||||
|     | ToggleSettings | ||||
|     | SetSettings Settings | ||||
|     | SetPlaybackRate Float | ||||
|     | SetQuality Quality.Quality | ||||
|     | SetVolume Float Bool | ||||
|     | RequestFullscreen | ||||
|     | ExitFullscreen | ||||
|     | AnimationFrameDelta Float | ||||
|     | MouseMove | ||||
|     | NowPlaying | ||||
|     | NowPaused | ||||
|     | NowHasDuration Float | ||||
|     | NowAtPosition Float | ||||
|     | NowAtVolume Float Bool | ||||
|     | NowLoaded (List ( Float, Float )) | ||||
|     | NowIsFullscreen Bool | ||||
|     | NowHasQualities (List Int) | ||||
|     | NowHasQuality Quality.Quality | ||||
|     | NowHasVideoSize ( Int, Int ) | ||||
|     | NowHasWindowSize ( Int, Int ) | ||||
|     | NowHasPlaybackRate Float | ||||
|     | VideoMsg Video.Msg | ||||
|     | NowHasScreenSize ( Int, Int ) | ||||
| 
 | ||||
| 
 | ||||
| init : Decode.Value -> ( Model, Cmd Msg ) | ||||
| @ -116,25 +72,8 @@ init flags = | ||||
|             Decode.decodeValue (Decode.field "height" Decode.int) flags | ||||
|                 |> Result.withDefault 0 | ||||
|     in | ||||
|     ( Model | ||||
|         url | ||||
|         False | ||||
|         0.0 | ||||
|         1.0 | ||||
|         [] | ||||
|         1.0 | ||||
|         False | ||||
|         False | ||||
|         Nothing | ||||
|         [] | ||||
|         True | ||||
|         0 | ||||
|         ( 0, 0 ) | ||||
|         ( width, height ) | ||||
|         1.0 | ||||
|         All | ||||
|         False | ||||
|     , initVideo url | ||||
|     ( { video = Video.fromUrl url, screenSize = ( width, height ) } | ||||
|     , Video.init url | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @ -144,708 +83,22 @@ update msg model = | ||||
|         Noop -> | ||||
|             ( model, Cmd.none ) | ||||
| 
 | ||||
|         PlayPause -> | ||||
|             ( model, playPause () ) | ||||
|         VideoMsg m -> | ||||
|             let | ||||
|                 ( video, cmd ) = | ||||
|                     Video.update m model.video | ||||
|             in | ||||
|             ( { model | video = video }, Cmd.map VideoMsg cmd ) | ||||
| 
 | ||||
|         Seek time -> | ||||
|             ( model, seek time ) | ||||
| 
 | ||||
|         SetPlaybackRate rate -> | ||||
|             ( { model | showSettings = False, settings = All }, setPlaybackRate rate ) | ||||
| 
 | ||||
|         ToggleSettings -> | ||||
|             ( { model | showSettings = not model.showSettings }, Cmd.none ) | ||||
| 
 | ||||
|         SetSettings s -> | ||||
|             ( { model | settings = s }, Cmd.none ) | ||||
| 
 | ||||
|         RequestFullscreen -> | ||||
|             ( model, requestFullscreen () ) | ||||
| 
 | ||||
|         ExitFullscreen -> | ||||
|             ( model, exitFullscreen () ) | ||||
| 
 | ||||
|         SetQuality q -> | ||||
|             ( { model | showSettings = False, settings = All }, setQuality q ) | ||||
| 
 | ||||
|         SetVolume v m -> | ||||
|             ( model, setVolume { volume = v, muted = m } ) | ||||
| 
 | ||||
|         AnimationFrameDelta delta -> | ||||
|             if model.animationFrame + delta > 3500 then | ||||
|                 ( { model | animationFrame = model.animationFrame + delta, showSettings = False, settings = All }, Cmd.none ) | ||||
| 
 | ||||
|             else | ||||
|                 ( { model | animationFrame = model.animationFrame + delta }, Cmd.none ) | ||||
| 
 | ||||
|         MouseMove -> | ||||
|             ( { model | animationFrame = 0 }, Cmd.none ) | ||||
| 
 | ||||
|         NowPlaying -> | ||||
|             ( { model | playing = True }, Cmd.none ) | ||||
| 
 | ||||
|         NowPaused -> | ||||
|             ( { model | playing = False }, Cmd.none ) | ||||
| 
 | ||||
|         NowHasDuration duration -> | ||||
|             ( { model | duration = duration }, Cmd.none ) | ||||
| 
 | ||||
|         NowAtPosition position -> | ||||
|             ( { model | position = position }, Cmd.none ) | ||||
| 
 | ||||
|         NowAtVolume volume muted -> | ||||
|             ( { model | volume = volume, muted = muted }, Cmd.none ) | ||||
| 
 | ||||
|         NowLoaded loaded -> | ||||
|             ( { model | loaded = loaded }, Cmd.none ) | ||||
| 
 | ||||
|         NowIsFullscreen fullscreen -> | ||||
|             ( { model | isFullscreen = fullscreen }, Cmd.none ) | ||||
| 
 | ||||
|         NowHasQualities qualities -> | ||||
|             ( { model | qualities = qualities }, Cmd.none ) | ||||
| 
 | ||||
|         NowHasQuality quality -> | ||||
|             ( { model | quality = Just quality }, Cmd.none ) | ||||
| 
 | ||||
|         NowHasVideoSize size -> | ||||
|             ( { model | videoSize = size }, Cmd.none ) | ||||
| 
 | ||||
|         NowHasWindowSize size -> | ||||
|         NowHasScreenSize size -> | ||||
|             ( { model | screenSize = size }, Cmd.none ) | ||||
| 
 | ||||
|         NowHasPlaybackRate rate -> | ||||
|             ( { model | playbackRate = rate }, Cmd.none ) | ||||
| 
 | ||||
| 
 | ||||
| view : Model -> Browser.Document Msg | ||||
| view model = | ||||
|     { title = "Hello" | ||||
|     , body = [ Element.layout [ Element.height Element.fill ] (video model) ] | ||||
|     , body = | ||||
|         [ Element.layout [ Element.height Element.fill ] | ||||
|             (Element.map VideoMsg (Views.embed model.screenSize model.video)) | ||||
|         ] | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| video : Model -> Element Msg | ||||
| video 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) | ||||
| 
 | ||||
|         bar = | ||||
|             animatedEl | ||||
|                 (if model.animationFrame < 3000 then | ||||
|                     fadeIn | ||||
| 
 | ||||
|                  else | ||||
|                     fadeOut | ||||
|                 ) | ||||
|                 [ Element.width Element.fill, Element.height Element.fill ] | ||||
|                 (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 | ||||
|                                         :: seekBarEvents model | ||||
|                                     ) | ||||
|                                     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.videoSize) / toFloat (Tuple.second model.videoSize) | ||||
| 
 | ||||
|         screenAspectRatio = | ||||
|             toFloat (Tuple.first model.screenSize) / toFloat (Tuple.second model.screenSize) | ||||
| 
 | ||||
|         ( ( x, y ), ( w, h ) ) = | ||||
|             if videoAspectRatio > screenAspectRatio then | ||||
|                 let | ||||
|                     videoHeight = | ||||
|                         Tuple.first model.screenSize * Tuple.second model.videoSize // Tuple.first model.videoSize | ||||
|                 in | ||||
|                 ( ( 0, (Tuple.second model.screenSize - videoHeight) // 2 ) | ||||
|                 , ( Tuple.first model.screenSize, videoHeight ) | ||||
|                 ) | ||||
| 
 | ||||
|             else | ||||
|                 let | ||||
|                     videoWidth = | ||||
|                         Tuple.second model.screenSize * Tuple.first model.videoSize // Tuple.second model.videoSize | ||||
|                 in | ||||
|                 ( ( (Tuple.first model.screenSize - videoWidth) // 2, 0 ) | ||||
|                 , ( videoWidth, Tuple.second model.screenSize ) | ||||
|                 ) | ||||
|     in | ||||
|     Element.el | ||||
|         (Element.inFront bar | ||||
|             :: Element.width Element.fill | ||||
|             :: Element.height Element.fill | ||||
|             :: Background.color (Element.rgb 0 0 0) | ||||
|             :: Element.htmlAttribute (Html.Attributes.id "full") | ||||
|             :: playerEvents | ||||
|         ) | ||||
|         (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") | ||||
|                     :: videoEvents | ||||
|                 ) | ||||
|                 [] | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| settings : Model -> Element Msg | ||||
| settings model = | ||||
|     let | ||||
|         makeMenuButton : Settings -> Element Msg -> Element Msg -> Element 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 (SetSettings s) | ||||
|                 } | ||||
| 
 | ||||
|         speedButton = | ||||
|             makeMenuButton Speed (Element.text "Speed") (Element.text ("x" ++ String.fromFloat model.playbackRate)) | ||||
| 
 | ||||
|         qualityButton = | ||||
|             case model.quality of | ||||
|                 Just q -> | ||||
|                     makeMenuButton Quality (Element.text "Quality") (Element.text (Quality.toString q)) | ||||
| 
 | ||||
|                 _ -> | ||||
|                     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 (SetSettings 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 (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 (SetQuality { auto = x == 0, height = x }) | ||||
|                             } | ||||
|                     ) | ||||
|                 |> (\x -> returnButton :: x) | ||||
| 
 | ||||
|         buttons = | ||||
|             case model.settings of | ||||
|                 All -> | ||||
|                     [ speedButton, qualityButton ] | ||||
| 
 | ||||
|                 Speed -> | ||||
|                     speedOptions | ||||
| 
 | ||||
|                 Quality -> | ||||
|                     qualityOptions | ||||
|     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 | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| playPauseButton : Bool -> Element Msg | ||||
| playPauseButton playing = | ||||
|     let | ||||
|         icon = | ||||
|             if playing then | ||||
|                 Icons.pause True | ||||
| 
 | ||||
|             else | ||||
|                 Icons.play True | ||||
|     in | ||||
|     Input.button [] | ||||
|         { label = icon | ||||
|         , onPress = Just PlayPause | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| fullscreenButton : Bool -> Element Msg | ||||
| fullscreenButton isFullscreen = | ||||
|     Input.button [] | ||||
|         (if isFullscreen then | ||||
|             { label = Icons.minimize False | ||||
|             , onPress = Just ExitFullscreen | ||||
|             } | ||||
| 
 | ||||
|          else | ||||
|             { label = Icons.maximize False | ||||
|             , onPress = Just RequestFullscreen | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| volumeButton : Float -> Bool -> Element 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 (SetVolume volume (not muted)) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| settingsButton : Element Msg | ||||
| settingsButton = | ||||
|     Input.button [] | ||||
|         { label = Icons.settings False | ||||
|         , onPress = Just ToggleSettings | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| playerEvents : List (Element.Attribute Msg) | ||||
| playerEvents = | ||||
|     List.map Element.htmlAttribute | ||||
|         [ Html.Events.on "fullscreenchange" decodeFullscreenChange | ||||
|         , Html.Events.on "mousemove" (Decode.succeed MouseMove) | ||||
|         ] | ||||
| 
 | ||||
| 
 | ||||
| videoEvents : List (Html.Attribute Msg) | ||||
| videoEvents = | ||||
|     [ Html.Attributes.id "video" | ||||
|     , Html.Events.on "playing" (Decode.succeed NowPlaying) | ||||
|     , Html.Events.on "pause" (Decode.succeed NowPaused) | ||||
|     , Html.Events.on "durationchange" decodeDurationChanged | ||||
|     , Html.Events.on "timeupdate" decodePosition | ||||
|     , Html.Events.on "volumechange" decodeVolumeChange | ||||
|     , Html.Events.on "progress" decodeProgress | ||||
|     , Html.Events.on "resize" decodeVideoResize | ||||
|     , Html.Events.on "ratechange" decodePlaybackRateChange | ||||
|     ] | ||||
| 
 | ||||
| 
 | ||||
| seekBarEvents : Model -> List (Element.Attribute Msg) | ||||
| seekBarEvents model = | ||||
|     List.map Element.htmlAttribute | ||||
|         [ Html.Events.on "click" (decodeSeek model) | ||||
|         ] | ||||
| 
 | ||||
| 
 | ||||
| decodeDurationChanged : Decode.Decoder Msg | ||||
| decodeDurationChanged = | ||||
|     Dom.target <| | ||||
|         Decode.map NowHasDuration | ||||
|             (Decode.field "duration" Decode.float) | ||||
| 
 | ||||
| 
 | ||||
| decodePosition : Decode.Decoder Msg | ||||
| decodePosition = | ||||
|     Dom.target <| | ||||
|         Decode.map NowAtPosition | ||||
|             (Decode.field "currentTime" Decode.float) | ||||
| 
 | ||||
| 
 | ||||
| decodeVolumeChange : Decode.Decoder Msg | ||||
| decodeVolumeChange = | ||||
|     Dom.target <| | ||||
|         Decode.map2 NowAtVolume | ||||
|             (Decode.field "volume" Decode.float) | ||||
|             (Decode.field "muted" Decode.bool) | ||||
| 
 | ||||
| 
 | ||||
| decodeSeek : Model -> Decode.Decoder Msg | ||||
| decodeSeek model = | ||||
|     Decode.map2 (\x y -> Seek (toFloat x / toFloat y * model.duration)) | ||||
|         (Decode.field "layerX" Decode.int) | ||||
|         (Dom.target <| Decode.field "offsetWidth" Decode.int) | ||||
| 
 | ||||
| 
 | ||||
| decodeProgress : Decode.Decoder Msg | ||||
| decodeProgress = | ||||
|     decodeTimeRanges | ||||
|         |> Decode.field "asArray" | ||||
|         |> Decode.field "buffered" | ||||
|         |> Dom.target | ||||
|         |> Decode.map NowLoaded | ||||
| 
 | ||||
| 
 | ||||
| decodeTimeRanges : Decode.Decoder (List ( Float, Float )) | ||||
| decodeTimeRanges = | ||||
|     Decode.list decodeTimeRange | ||||
| 
 | ||||
| 
 | ||||
| decodeTimeRange : Decode.Decoder ( Float, Float ) | ||||
| decodeTimeRange = | ||||
|     Decode.map2 Tuple.pair | ||||
|         (Decode.field "start" Decode.float) | ||||
|         (Decode.field "end" Decode.float) | ||||
| 
 | ||||
| 
 | ||||
| decodeFullscreenChange : Decode.Decoder Msg | ||||
| decodeFullscreenChange = | ||||
|     Decode.value | ||||
|         |> Decode.nullable | ||||
|         |> Decode.field "fullscreenElement" | ||||
|         |> Decode.field "document" | ||||
|         |> Dom.target | ||||
|         |> Decode.map (\x -> NowIsFullscreen (x /= Nothing)) | ||||
| 
 | ||||
| 
 | ||||
| decodeVideoResize : Decode.Decoder Msg | ||||
| decodeVideoResize = | ||||
|     Dom.target <| | ||||
|         Decode.map2 (\x y -> NowHasVideoSize ( x, y )) | ||||
|             (Decode.field "videoWidth" Decode.int) | ||||
|             (Decode.field "videoHeight" Decode.int) | ||||
| 
 | ||||
| 
 | ||||
| decodePlaybackRateChange : Decode.Decoder Msg | ||||
| decodePlaybackRateChange = | ||||
|     Dom.target <| | ||||
|         Decode.map NowHasPlaybackRate | ||||
|             (Decode.field "playbackRate" Decode.float) | ||||
| 
 | ||||
| 
 | ||||
| decodeKeyDown : Model -> Decode.Decoder Msg | ||||
| decodeKeyDown model = | ||||
|     Decode.field "keyCode" Decode.int | ||||
|         |> Decode.andThen | ||||
|             (\x -> | ||||
|                 case x of | ||||
|                     -- Enter key | ||||
|                     32 -> | ||||
|                         Decode.succeed PlayPause | ||||
| 
 | ||||
|                     -- J key | ||||
|                     74 -> | ||||
|                         Decode.succeed (Seek (max 0 (model.position - 10))) | ||||
| 
 | ||||
|                     -- L key | ||||
|                     76 -> | ||||
|                         Decode.succeed (Seek (min model.duration (model.position + 10))) | ||||
| 
 | ||||
|                     -- K key | ||||
|                     75 -> | ||||
|                         Decode.succeed PlayPause | ||||
| 
 | ||||
|                     -- Left arrow | ||||
|                     37 -> | ||||
|                         Decode.succeed (Seek (max 0 (model.position - 5))) | ||||
| 
 | ||||
|                     -- Right arrow | ||||
|                     39 -> | ||||
|                         Decode.succeed (Seek (min model.duration (model.position + 5))) | ||||
| 
 | ||||
|                     -- Down arrow | ||||
|                     40 -> | ||||
|                         Decode.succeed (SetVolume (max 0 (model.volume - 0.1)) model.muted) | ||||
| 
 | ||||
|                     -- Top arrow | ||||
|                     38 -> | ||||
|                         Decode.succeed (SetVolume (min 1 (model.volume + 0.1)) model.muted) | ||||
| 
 | ||||
|                     -- M key | ||||
|                     77 -> | ||||
|                         Decode.succeed (SetVolume model.volume (not model.muted)) | ||||
| 
 | ||||
|                     -- F key | ||||
|                     70 -> | ||||
|                         Decode.succeed | ||||
|                             (if model.isFullscreen then | ||||
|                                 ExitFullscreen | ||||
| 
 | ||||
|                              else | ||||
|                                 RequestFullscreen | ||||
|                             ) | ||||
| 
 | ||||
|                     _ -> | ||||
|                         Decode.fail ("no shortcut for code " ++ String.fromInt x) | ||||
|             ) | ||||
| 
 | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
| 
 | ||||
| port initVideo : String -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| port playPause : () -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| port seek : Float -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| port requestFullscreen : () -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| port exitFullscreen : () -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| port setPlaybackRate : Float -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| port setQuality : Quality.Quality -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| port setVolume : { volume : Float, muted : Bool } -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| port nowHasQualities : (List Int -> msg) -> Sub msg | ||||
| 
 | ||||
| 
 | ||||
| port nowHasQuality : (Decode.Value -> msg) -> Sub msg | ||||
| 
 | ||||
| 
 | ||||
| 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 ] | ||||
| 
 | ||||
| 
 | ||||
| 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 | ||||
|         } | ||||
|  | ||||
							
								
								
									
										234
									
								
								src/Video.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								src/Video.elm
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,234 @@ | ||||
| port module Video exposing (Msg(..), Settings(..), Video, fromUrl, init, nowHasQualities, nowHasQuality, update) | ||||
| 
 | ||||
| import Json.Decode as Decode | ||||
| import Quality exposing (Quality) | ||||
| 
 | ||||
| 
 | ||||
| type alias Video = | ||||
|     { url : String | ||||
|     , playing : Bool | ||||
|     , position : Float | ||||
|     , duration : Float | ||||
|     , loaded : List ( Float, Float ) | ||||
|     , volume : Float | ||||
|     , muted : Bool | ||||
|     , isFullscreen : Bool | ||||
|     , quality : Maybe Quality.Quality | ||||
|     , qualities : List Int | ||||
|     , showBar : Bool | ||||
|     , animationFrame : Float | ||||
|     , size : ( Int, Int ) | ||||
|     , playbackRate : Float | ||||
|     , settings : Settings | ||||
|     , showSettings : Bool | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 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 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| type Settings | ||||
|     = All | ||||
|     | Speed | ||||
|     | Quality | ||||
| 
 | ||||
| 
 | ||||
| type Msg | ||||
|     = Noop | ||||
|     | PlayPause | ||||
|     | Seek Float | ||||
|     | ToggleSettings | ||||
|     | SetSettings Settings | ||||
|     | SetPlaybackRate Float | ||||
|     | SetQuality Quality.Quality | ||||
|     | SetVolume Float Bool | ||||
|     | RequestFullscreen | ||||
|     | ExitFullscreen | ||||
|     | AnimationFrameDelta Float | ||||
|     | MouseMove | ||||
|     | NowPlaying | ||||
|     | NowPaused | ||||
|     | NowHasDuration Float | ||||
|     | NowAtPosition Float | ||||
|     | NowAtVolume Float Bool | ||||
|     | NowLoaded (List ( Float, Float )) | ||||
|     | NowIsFullscreen Bool | ||||
|     | NowHasQualities (List Int) | ||||
|     | NowHasQuality Quality.Quality | ||||
|     | NowHasSize ( Int, Int ) | ||||
|     | NowHasPlaybackRate Float | ||||
| 
 | ||||
| 
 | ||||
| update : Msg -> Video -> ( Video, Cmd Msg ) | ||||
| update msg model = | ||||
|     case msg of | ||||
|         Noop -> | ||||
|             ( model, Cmd.none ) | ||||
| 
 | ||||
|         PlayPause -> | ||||
|             ( model, playPause ) | ||||
| 
 | ||||
|         Seek time -> | ||||
|             ( model, seek time ) | ||||
| 
 | ||||
|         SetPlaybackRate rate -> | ||||
|             ( { model | showSettings = False, settings = All }, setPlaybackRate rate ) | ||||
| 
 | ||||
|         ToggleSettings -> | ||||
|             ( { model | showSettings = not model.showSettings }, Cmd.none ) | ||||
| 
 | ||||
|         SetSettings s -> | ||||
|             ( { model | settings = s }, Cmd.none ) | ||||
| 
 | ||||
|         RequestFullscreen -> | ||||
|             ( model, requestFullscreen ) | ||||
| 
 | ||||
|         ExitFullscreen -> | ||||
|             ( model, exitFullscreen ) | ||||
| 
 | ||||
|         SetQuality q -> | ||||
|             ( { model | showSettings = False, settings = All }, setQuality q ) | ||||
| 
 | ||||
|         SetVolume v m -> | ||||
|             ( model, setVolume { volume = v, muted = m } ) | ||||
| 
 | ||||
|         AnimationFrameDelta delta -> | ||||
|             if model.animationFrame + delta > 3500 then | ||||
|                 ( { model | animationFrame = model.animationFrame + delta, showSettings = False, settings = All }, Cmd.none ) | ||||
| 
 | ||||
|             else | ||||
|                 ( { model | animationFrame = model.animationFrame + delta }, Cmd.none ) | ||||
| 
 | ||||
|         MouseMove -> | ||||
|             ( { model | animationFrame = 0 }, Cmd.none ) | ||||
| 
 | ||||
|         NowPlaying -> | ||||
|             ( { model | playing = True }, Cmd.none ) | ||||
| 
 | ||||
|         NowPaused -> | ||||
|             ( { model | playing = False }, Cmd.none ) | ||||
| 
 | ||||
|         NowHasDuration duration -> | ||||
|             ( { model | duration = duration }, Cmd.none ) | ||||
| 
 | ||||
|         NowAtPosition position -> | ||||
|             ( { model | position = position }, Cmd.none ) | ||||
| 
 | ||||
|         NowAtVolume volume muted -> | ||||
|             ( { model | volume = volume, muted = muted }, Cmd.none ) | ||||
| 
 | ||||
|         NowLoaded loaded -> | ||||
|             ( { model | loaded = loaded }, Cmd.none ) | ||||
| 
 | ||||
|         NowIsFullscreen fullscreen -> | ||||
|             ( { model | isFullscreen = fullscreen }, Cmd.none ) | ||||
| 
 | ||||
|         NowHasQualities qualities -> | ||||
|             ( { model | qualities = qualities }, Cmd.none ) | ||||
| 
 | ||||
|         NowHasQuality quality -> | ||||
|             ( { model | quality = Just quality }, Cmd.none ) | ||||
| 
 | ||||
|         NowHasSize size -> | ||||
|             ( { model | size = size }, Cmd.none ) | ||||
| 
 | ||||
|         NowHasPlaybackRate rate -> | ||||
|             ( { model | playbackRate = rate }, Cmd.none ) | ||||
| 
 | ||||
| 
 | ||||
| port polymnyVideoInit : String -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| init : String -> Cmd msg | ||||
| init = | ||||
|     polymnyVideoInit | ||||
| 
 | ||||
| 
 | ||||
| port polymnyVideoPlayPause : () -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| playPause : Cmd msg | ||||
| playPause = | ||||
|     polymnyVideoPlayPause () | ||||
| 
 | ||||
| 
 | ||||
| port polymnyVideoSeek : Float -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| seek : Float -> Cmd msg | ||||
| seek = | ||||
|     polymnyVideoSeek | ||||
| 
 | ||||
| 
 | ||||
| port polymnyVideoRequestFullscreen : () -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| requestFullscreen : Cmd msg | ||||
| requestFullscreen = | ||||
|     polymnyVideoRequestFullscreen () | ||||
| 
 | ||||
| 
 | ||||
| port polymnyVideoExitFullscreen : () -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| exitFullscreen : Cmd msg | ||||
| exitFullscreen = | ||||
|     polymnyVideoExitFullscreen () | ||||
| 
 | ||||
| 
 | ||||
| port polymnyVideoSetPlaybackRate : Float -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| setPlaybackRate : Float -> Cmd msg | ||||
| setPlaybackRate = | ||||
|     polymnyVideoSetPlaybackRate | ||||
| 
 | ||||
| 
 | ||||
| port polymnyVideoSetQuality : Quality -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| setQuality : Quality -> Cmd msg | ||||
| setQuality = | ||||
|     polymnyVideoSetQuality | ||||
| 
 | ||||
| 
 | ||||
| port polymnyVideoSetVolume : { volume : Float, muted : Bool } -> Cmd msg | ||||
| 
 | ||||
| 
 | ||||
| setVolume : { volume : Float, muted : Bool } -> Cmd msg | ||||
| setVolume = | ||||
|     polymnyVideoSetVolume | ||||
| 
 | ||||
| 
 | ||||
| port polymnyVideoNowHasQualities : (List Int -> msg) -> Sub msg | ||||
| 
 | ||||
| 
 | ||||
| nowHasQualities : (List Int -> msg) -> Sub msg | ||||
| nowHasQualities = | ||||
|     polymnyVideoNowHasQualities | ||||
| 
 | ||||
| 
 | ||||
| port polymnyVideoNowHasQuality : (Decode.Value -> msg) -> Sub msg | ||||
| 
 | ||||
| 
 | ||||
| nowHasQuality : (Decode.Value -> msg) -> Sub msg | ||||
| nowHasQuality = | ||||
|     polymnyVideoNowHasQuality | ||||
							
								
								
									
										451
									
								
								src/Views.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										451
									
								
								src/Views.elm
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,451 @@ | ||||
| module 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) | ||||
| 
 | ||||
| 
 | ||||
| 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) | ||||
| 
 | ||||
|         bar = | ||||
|             animatedEl | ||||
|                 (if model.animationFrame < 3000 then | ||||
|                     fadeIn | ||||
| 
 | ||||
|                  else | ||||
|                     fadeOut | ||||
|                 ) | ||||
|                 [ Element.width Element.fill, Element.height Element.fill ] | ||||
|                 (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.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) | ||||
| 
 | ||||
|         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 bar | ||||
|             :: Element.width Element.fill | ||||
|             :: Element.height Element.fill | ||||
|             :: Background.color (Element.rgb 0 0 0) | ||||
|             :: Element.htmlAttribute (Html.Attributes.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 | ||||
|                 ) | ||||
|                 [] | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
|         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) | ||||
| 
 | ||||
|         buttons = | ||||
|             case model.settings of | ||||
|                 Video.All -> | ||||
|                     [ speedButton, qualityButton ] | ||||
| 
 | ||||
|                 Video.Speed -> | ||||
|                     speedOptions | ||||
| 
 | ||||
|                 Video.Quality -> | ||||
|                     qualityOptions | ||||
|     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 | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| 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 ] | ||||
| 
 | ||||
| 
 | ||||
| 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 | ||||
|         } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user