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;
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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…
Reference in New Issue