Started cleaning
This commit is contained in:
		
							parent
							
								
									37a8253387
								
							
						
					
					
						commit
						3193f3c382
					
				
							
								
								
									
										22
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								index.html
									
									
									
									
									
								
							| @ -42,7 +42,7 @@ | |||||||
| 
 | 
 | ||||||
|             let hls; |             let hls; | ||||||
| 
 | 
 | ||||||
|             app.ports.initVideo.subscribe(function(arg) { |             app.ports.polymnyVideoInit.subscribe(function(arg) { | ||||||
|                 const video = document.getElementById('video'); |                 const video = document.getElementById('video'); | ||||||
|                 if (Hls.isSupported()) { |                 if (Hls.isSupported()) { | ||||||
|                     hls = new Hls(); |                     hls = new Hls(); | ||||||
| @ -52,11 +52,11 @@ | |||||||
|                         // Transform available levels into an array of integers (height values). |                         // Transform available levels into an array of integers (height values). | ||||||
|                         const availableQualities = hls.levels.map((l) => l.height); |                         const availableQualities = hls.levels.map((l) => l.height); | ||||||
|                         availableQualities.unshift(0); |                         availableQualities.unshift(0); | ||||||
|                         app.ports.nowHasQualities.send(availableQualities); |                         app.ports.polymnyVideoNowHasQualities.send(availableQualities); | ||||||
|                     }); |                     }); | ||||||
| 
 | 
 | ||||||
|                     hls.on(Hls.Events.LEVEL_SWITCHED, function (event, data) { |                     hls.on(Hls.Events.LEVEL_SWITCHED, function (event, data) { | ||||||
|                         app.ports.nowHasQuality.send({ |                         app.ports.polymnyVideoNowHasQuality.send({ | ||||||
|                             auto: hls.autoLevelEnabled, |                             auto: hls.autoLevelEnabled, | ||||||
|                             height: hls.levels[data.level].height |                             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'); |                 const video = document.getElementById('video'); | ||||||
|                 if (video.paused) { |                 if (video.paused) { | ||||||
|                     video.play(); |                     video.play(); | ||||||
| @ -77,26 +77,26 @@ | |||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             app.ports.seek.subscribe(function(arg) { |             app.ports.polymnyVideoSeek.subscribe(function(arg) { | ||||||
|                 const video = document.getElementById('video'); |                 const video = document.getElementById('video'); | ||||||
|                 console.log(arg); |                 console.log(arg); | ||||||
|                 video.currentTime = arg; |                 video.currentTime = arg; | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             app.ports.requestFullscreen.subscribe(function() { |             app.ports.polymnyVideoRequestFullscreen.subscribe(function() { | ||||||
|                 document.getElementById('full').requestFullscreen(); |                 document.getElementById('full').requestFullscreen(); | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             app.ports.exitFullscreen.subscribe(function() { |             app.ports.polymnyVideoExitFullscreen.subscribe(function() { | ||||||
|                 document.exitFullscreen(); |                 document.exitFullscreen(); | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             app.ports.setPlaybackRate.subscribe(function(arg) { |             app.ports.polymnyVideoSetPlaybackRate.subscribe(function(arg) { | ||||||
|                 const video = document.getElementById('video'); |                 const video = document.getElementById('video'); | ||||||
|                 video.playbackRate = arg; |                 video.playbackRate = arg; | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             app.ports.setQuality.subscribe(function(arg) { |             app.ports.polymnyVideoSetQuality.subscribe(function(arg) { | ||||||
|                 var old = hls.currentLevel; |                 var old = hls.currentLevel; | ||||||
|                 if (arg.auto) { |                 if (arg.auto) { | ||||||
|                     hls.currentLevel = -1; |                     hls.currentLevel = -1; | ||||||
| @ -108,14 +108,14 @@ | |||||||
|                     }); |                     }); | ||||||
|                 } |                 } | ||||||
|                 if (old === hls.currentLevel) { |                 if (old === hls.currentLevel) { | ||||||
|                     app.ports.nowHasQuality.send({ |                     app.ports.polymnyVideoNowHasQuality.send({ | ||||||
|                         auto: hls.autoLevelEnabled, |                         auto: hls.autoLevelEnabled, | ||||||
|                         height: hls.currentLevel === -1 ? 0 : hls.levels[hls.currentLevel].height |                         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'); |                 const video = document.getElementById('video'); | ||||||
|                 video.volume = arg.volume; |                 video.volume = arg.volume; | ||||||
|                 video.muted = arg.muted; |                 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 | ||||||
| import Browser.Events | import Browser.Events | ||||||
| @ -8,6 +8,7 @@ import Element.Background as Background | |||||||
| import Element.Border as Border | import Element.Border as Border | ||||||
| import Element.Font as Font | import Element.Font as Font | ||||||
| import Element.Input as Input | import Element.Input as Input | ||||||
|  | import Events | ||||||
| import Html | import Html | ||||||
| import Html.Attributes | import Html.Attributes | ||||||
| import Html.Events | import Html.Events | ||||||
| @ -17,6 +18,8 @@ import Quality | |||||||
| import Simple.Animation as Animation exposing (Animation) | import Simple.Animation as Animation exposing (Animation) | ||||||
| import Simple.Animation.Animated as Animated | import Simple.Animation.Animated as Animated | ||||||
| import Simple.Animation.Property as P | import Simple.Animation.Property as P | ||||||
|  | import Video exposing (Video) | ||||||
|  | import Views | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| main : Program Decode.Value Model Msg | main : Program Decode.Value Model Msg | ||||||
| @ -28,19 +31,8 @@ main = | |||||||
|         , subscriptions = |         , subscriptions = | ||||||
|             \model -> |             \model -> | ||||||
|                 Sub.batch |                 Sub.batch | ||||||
|                     [ nowHasQualities NowHasQualities |                     [ Events.subs model.video |> Sub.map VideoMsg | ||||||
|                     , nowHasQuality |                     , Browser.Events.onResize (\x y -> NowHasScreenSize ( x, y )) | ||||||
|                         (\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) |  | ||||||
|                     ] |                     ] | ||||||
|         , onUrlChange = \_ -> Noop |         , onUrlChange = \_ -> Noop | ||||||
|         , onUrlRequest = \_ -> Noop |         , onUrlRequest = \_ -> Noop | ||||||
| @ -48,23 +40,8 @@ main = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| type alias Model = | type alias Model = | ||||||
|     { url : String |     { video : Video | ||||||
|     , 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 ) |  | ||||||
|     , screenSize : ( Int, Int ) |     , screenSize : ( Int, Int ) | ||||||
|     , playbackRate : Float |  | ||||||
|     , settings : Settings |  | ||||||
|     , showSettings : Bool |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -76,29 +53,8 @@ type Settings | |||||||
| 
 | 
 | ||||||
| type Msg | type Msg | ||||||
|     = Noop |     = Noop | ||||||
|     | PlayPause |     | VideoMsg Video.Msg | ||||||
|     | Seek Float |     | NowHasScreenSize ( Int, Int ) | ||||||
|     | 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 |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| init : Decode.Value -> ( Model, Cmd Msg ) | init : Decode.Value -> ( Model, Cmd Msg ) | ||||||
| @ -116,25 +72,8 @@ init flags = | |||||||
|             Decode.decodeValue (Decode.field "height" Decode.int) flags |             Decode.decodeValue (Decode.field "height" Decode.int) flags | ||||||
|                 |> Result.withDefault 0 |                 |> Result.withDefault 0 | ||||||
|     in |     in | ||||||
|     ( Model |     ( { video = Video.fromUrl url, screenSize = ( width, height ) } | ||||||
|         url |     , Video.init url | ||||||
|         False |  | ||||||
|         0.0 |  | ||||||
|         1.0 |  | ||||||
|         [] |  | ||||||
|         1.0 |  | ||||||
|         False |  | ||||||
|         False |  | ||||||
|         Nothing |  | ||||||
|         [] |  | ||||||
|         True |  | ||||||
|         0 |  | ||||||
|         ( 0, 0 ) |  | ||||||
|         ( width, height ) |  | ||||||
|         1.0 |  | ||||||
|         All |  | ||||||
|         False |  | ||||||
|     , initVideo url |  | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -144,708 +83,22 @@ update msg model = | |||||||
|         Noop -> |         Noop -> | ||||||
|             ( model, Cmd.none ) |             ( model, Cmd.none ) | ||||||
| 
 | 
 | ||||||
|         PlayPause -> |         VideoMsg m -> | ||||||
|             ( model, playPause () ) |             let | ||||||
|  |                 ( video, cmd ) = | ||||||
|  |                     Video.update m model.video | ||||||
|  |             in | ||||||
|  |             ( { model | video = video }, Cmd.map VideoMsg cmd ) | ||||||
| 
 | 
 | ||||||
|         Seek time -> |         NowHasScreenSize size -> | ||||||
|             ( 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 -> |  | ||||||
|             ( { model | screenSize = size }, Cmd.none ) |             ( { model | screenSize = size }, Cmd.none ) | ||||||
| 
 | 
 | ||||||
|         NowHasPlaybackRate rate -> |  | ||||||
|             ( { model | playbackRate = rate }, Cmd.none ) |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| view : Model -> Browser.Document Msg | view : Model -> Browser.Document Msg | ||||||
| view model = | view model = | ||||||
|     { title = "Hello" |     { 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