Cleaning, making generic
This commit is contained in:
parent
51dd1736fd
commit
e26971d995
4
Makefile
4
Makefile
|
@ -19,7 +19,7 @@ dev: js/main.js
|
||||||
release: js/main.min.js
|
release: js/main.min.js
|
||||||
|
|
||||||
js/main.js: src/**
|
js/main.js: src/**
|
||||||
$(ELM) make src/Main.elm --output $(BUILD_DIR)/main.js
|
$(ELM) make src/Examples/Embed.elm --output $(BUILD_DIR)/main.js
|
||||||
|
|
||||||
js/main.min.js: js/main.tmp.js
|
js/main.min.js: js/main.tmp.js
|
||||||
@$(UGLIFYJS) $(BUILD_DIR)/main.tmp.js --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle > $(BUILD_DIR)/main.min.js
|
@$(UGLIFYJS) $(BUILD_DIR)/main.tmp.js --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle > $(BUILD_DIR)/main.min.js
|
||||||
|
@ -28,7 +28,7 @@ js/main.tmp.js: src/**
|
||||||
@$(ELM) make src/Main.elm --optimize --output $(BUILD_DIR)/main.tmp.js
|
@$(ELM) make src/Main.elm --optimize --output $(BUILD_DIR)/main.tmp.js
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
@$(ELMLIVE) src/Main.elm -p 7000 -d . -- --output $(BUILD_DIR)/main.js
|
@$(ELMLIVE) src/Examples/Embed.elm -p 7000 -d . -- --output $(BUILD_DIR)/main.js
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@rm -rf $(BUILD_DIR)/{main.js,main.min.js}
|
@rm -rf $(BUILD_DIR)/{main.js,main.min.js}
|
||||||
|
|
|
@ -4,11 +4,6 @@
|
||||||
<title>twitch.tforgione.fr</title>
|
<title>twitch.tforgione.fr</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<style>
|
|
||||||
.filled {
|
|
||||||
fill: currentColor;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container"></div>
|
<div id="container"></div>
|
||||||
|
|
44
js/ports.js
44
js/ports.js
|
@ -27,16 +27,20 @@ function embed(options) {
|
||||||
options.width = window.innerWidth;
|
options.width = window.innerWidth;
|
||||||
options.height = window.innerHeight;
|
options.height = window.innerHeight;
|
||||||
|
|
||||||
const app = Elm.Main.init(options);
|
const app = Elm.Examples.Embed.init(options);
|
||||||
|
setupApp(app);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupApp(app) {
|
||||||
let hls;
|
let hls;
|
||||||
|
|
||||||
app.ports.polymnyVideoInit.subscribe(function(arg) {
|
app.ports.polymnyVideoInit.subscribe(function(arg) {
|
||||||
const video = document.getElementById('video');
|
const video = document.getElementById(arg[0]);
|
||||||
if (Hls.isSupported()) {
|
if (Hls.isSupported()) {
|
||||||
hls = new Hls();
|
hls = new Hls();
|
||||||
window.hls = hls;
|
window.hls = hls;
|
||||||
hls.loadSource(arg);
|
hls.loadSource(arg[1]);
|
||||||
|
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, function(event, data) {
|
hls.on(Hls.Events.MANIFEST_PARSED, function(event, data) {
|
||||||
const availableQualities = hls.levels.map((l) => l.height);
|
const availableQualities = hls.levels.map((l) => l.height);
|
||||||
|
@ -61,12 +65,12 @@ function embed(options) {
|
||||||
|
|
||||||
hls.attachMedia(video);
|
hls.attachMedia(video);
|
||||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
video.src = arg;
|
video.src = arg[1];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.ports.polymnyVideoPlayPause.subscribe(function() {
|
app.ports.polymnyVideoPlayPause.subscribe(function(arg) {
|
||||||
const video = document.getElementById('video');
|
const video = document.getElementById(arg);
|
||||||
if (video.paused) {
|
if (video.paused) {
|
||||||
video.play();
|
video.play();
|
||||||
} else {
|
} else {
|
||||||
|
@ -75,30 +79,30 @@ function embed(options) {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.ports.polymnyVideoSeek.subscribe(function(arg) {
|
app.ports.polymnyVideoSeek.subscribe(function(arg) {
|
||||||
const video = document.getElementById('video');
|
const video = document.getElementById(arg[0]);
|
||||||
video.currentTime = arg;
|
video.currentTime = arg[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
app.ports.polymnyVideoRequestFullscreen.subscribe(function() {
|
app.ports.polymnyVideoRequestFullscreen.subscribe(function(arg) {
|
||||||
document.getElementById('full').requestFullscreen();
|
document.getElementById(arg + '-full').requestFullscreen();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.ports.polymnyVideoExitFullscreen.subscribe(function() {
|
app.ports.polymnyVideoExitFullscreen.subscribe(function(arg) {
|
||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.ports.polymnyVideoSetPlaybackRate.subscribe(function(arg) {
|
app.ports.polymnyVideoSetPlaybackRate.subscribe(function(arg) {
|
||||||
const video = document.getElementById('video');
|
const video = document.getElementById(arg[0]);
|
||||||
video.playbackRate = arg;
|
video.playbackRate = arg[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
app.ports.polymnyVideoSetQuality.subscribe(function(arg) {
|
app.ports.polymnyVideoSetQuality.subscribe(function(arg) {
|
||||||
var old = hls.currentLevel;
|
var old = hls.currentLevel;
|
||||||
if (arg.auto) {
|
if (arg[1].auto) {
|
||||||
hls.currentLevel = -1;
|
hls.currentLevel = -1;
|
||||||
} else {
|
} else {
|
||||||
hls.levels.forEach((level, levelIndex) => {
|
hls.levels.forEach((level, levelIndex) => {
|
||||||
if (level.height === arg.height) {
|
if (level.height === arg[1].height) {
|
||||||
hls.currentLevel = levelIndex;
|
hls.currentLevel = levelIndex;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -112,14 +116,14 @@ function embed(options) {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.ports.polymnyVideoSetVolume.subscribe(function(arg) {
|
app.ports.polymnyVideoSetVolume.subscribe(function(arg) {
|
||||||
const video = document.getElementById('video');
|
const video = document.getElementById(arg[0]);
|
||||||
video.volume = arg.volume;
|
video.volume = arg[1].volume;
|
||||||
video.muted = arg.muted;
|
video.muted = arg[1].muted;
|
||||||
});
|
});
|
||||||
|
|
||||||
app.ports.polymnyVideoSetSubtitleTrack.subscribe(function(arg) {
|
app.ports.polymnyVideoSetSubtitleTrack.subscribe(function(arg) {
|
||||||
hls.subtitleDisplay = arg !== -1;
|
hls.subtitleDisplay = arg[1] !== -1;
|
||||||
hls.subtitleTrack = arg;
|
hls.subtitleTrack = arg[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
module Main exposing (..)
|
module Examples.Embed exposing (..)
|
||||||
|
|
||||||
import Browser
|
import Browser
|
||||||
import Browser.Events
|
import Browser.Events
|
||||||
|
@ -7,18 +7,16 @@ 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
|
||||||
import Icons
|
|
||||||
import Json.Decode as Decode
|
import Json.Decode as Decode
|
||||||
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 Video exposing (Video)
|
||||||
import Views
|
import Video.Events as Events
|
||||||
|
import Video.Views as Views
|
||||||
|
|
||||||
|
|
||||||
main : Program Decode.Value Model Msg
|
main : Program Decode.Value Model Msg
|
||||||
|
@ -44,12 +42,6 @@ type alias Model =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Settings
|
|
||||||
= All
|
|
||||||
| Speed
|
|
||||||
| Quality
|
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= Noop
|
= Noop
|
||||||
| VideoMsg Video.Msg
|
| VideoMsg Video.Msg
|
||||||
|
@ -70,9 +62,12 @@ init flags =
|
||||||
height =
|
height =
|
||||||
Decode.decodeValue (Decode.field "height" Decode.int) flags
|
Decode.decodeValue (Decode.field "height" Decode.int) flags
|
||||||
|> Result.withDefault 0
|
|> Result.withDefault 0
|
||||||
|
|
||||||
|
( video, cmd ) =
|
||||||
|
Video.fromUrl url "video"
|
||||||
in
|
in
|
||||||
( { video = Video.fromUrl url, screenSize = ( width, height ) }
|
( { video = video, screenSize = ( width, height ) }
|
||||||
, Video.init url
|
, Cmd.map VideoMsg cmd
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
138
src/Video.elm
138
src/Video.elm
|
@ -13,13 +13,14 @@ port module Video exposing
|
||||||
)
|
)
|
||||||
|
|
||||||
import Element exposing (Element)
|
import Element exposing (Element)
|
||||||
import Icons
|
|
||||||
import Json.Decode as Decode
|
import Json.Decode as Decode
|
||||||
import Quality exposing (Quality)
|
import Video.Icons as Icons
|
||||||
|
import Video.Quality as Quality exposing (Quality)
|
||||||
|
|
||||||
|
|
||||||
type alias Video =
|
type alias Video =
|
||||||
{ url : String
|
{ url : String
|
||||||
|
, id : String
|
||||||
, playing : Bool
|
, playing : Bool
|
||||||
, position : Float
|
, position : Float
|
||||||
, duration : Float
|
, duration : Float
|
||||||
|
@ -43,30 +44,33 @@ type alias Video =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fromUrl : String -> Video
|
fromUrl : String -> String -> ( Video, Cmd Msg )
|
||||||
fromUrl url =
|
fromUrl url id =
|
||||||
{ url = url
|
( { url = url
|
||||||
, playing = False
|
, id = id
|
||||||
, position = 0
|
, playing = False
|
||||||
, duration = 0
|
, position = 0
|
||||||
, loaded = []
|
, duration = 0
|
||||||
, volume = 1
|
, loaded = []
|
||||||
, muted = False
|
, volume = 1
|
||||||
, isFullscreen = False
|
, muted = False
|
||||||
, quality = Nothing
|
, isFullscreen = False
|
||||||
, qualities = []
|
, quality = Nothing
|
||||||
, showBar = True
|
, qualities = []
|
||||||
, animationFrame = 0
|
, showBar = True
|
||||||
, size = ( 0, 0 )
|
, animationFrame = 0
|
||||||
, playbackRate = 1
|
, size = ( 0, 0 )
|
||||||
, settings = All
|
, playbackRate = 1
|
||||||
, showSettings = False
|
, settings = All
|
||||||
, subtitles = []
|
, showSettings = False
|
||||||
, subtitleTrack = Nothing
|
, subtitles = []
|
||||||
, showMiniature = Nothing
|
, subtitleTrack = Nothing
|
||||||
, showIcon = Nothing
|
, showMiniature = Nothing
|
||||||
, showIconRequested = Nothing
|
, showIcon = Nothing
|
||||||
}
|
, showIconRequested = Nothing
|
||||||
|
}
|
||||||
|
, init id url
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
type Settings
|
type Settings
|
||||||
|
@ -131,7 +135,7 @@ update msg model =
|
||||||
else
|
else
|
||||||
Just (Icons.play True)
|
Just (Icons.play True)
|
||||||
}
|
}
|
||||||
, playPause
|
, playPause model.id
|
||||||
)
|
)
|
||||||
|
|
||||||
Seek time ->
|
Seek time ->
|
||||||
|
@ -143,11 +147,11 @@ update msg model =
|
||||||
else
|
else
|
||||||
Just (Icons.rewind True)
|
Just (Icons.rewind True)
|
||||||
}
|
}
|
||||||
, seek time
|
, seek model.id time
|
||||||
)
|
)
|
||||||
|
|
||||||
SetPlaybackRate rate ->
|
SetPlaybackRate rate ->
|
||||||
( { model | showSettings = False, settings = All }, setPlaybackRate rate )
|
( { model | showSettings = False, settings = All }, setPlaybackRate model.id rate )
|
||||||
|
|
||||||
ToggleSettings ->
|
ToggleSettings ->
|
||||||
( { model | showSettings = not model.showSettings }, Cmd.none )
|
( { model | showSettings = not model.showSettings }, Cmd.none )
|
||||||
|
@ -156,16 +160,16 @@ update msg model =
|
||||||
( { model | settings = s }, Cmd.none )
|
( { model | settings = s }, Cmd.none )
|
||||||
|
|
||||||
RequestFullscreen ->
|
RequestFullscreen ->
|
||||||
( model, requestFullscreen )
|
( model, requestFullscreen model.id )
|
||||||
|
|
||||||
ExitFullscreen ->
|
ExitFullscreen ->
|
||||||
( model, exitFullscreen )
|
( model, exitFullscreen model.id )
|
||||||
|
|
||||||
SetQuality q ->
|
SetQuality q ->
|
||||||
( { model | showSettings = False, settings = All }, setQuality q )
|
( { model | showSettings = False, settings = All }, setQuality model.id q )
|
||||||
|
|
||||||
SetSubtitleTrack t ->
|
SetSubtitleTrack t ->
|
||||||
( { model | showSettings = False, settings = All }, setSubtitleTrack t )
|
( { model | showSettings = False, settings = All }, setSubtitleTrack model.id t )
|
||||||
|
|
||||||
SetVolume v m ->
|
SetVolume v m ->
|
||||||
( { model
|
( { model
|
||||||
|
@ -179,7 +183,7 @@ update msg model =
|
||||||
else
|
else
|
||||||
Just (Icons.volume1 True)
|
Just (Icons.volume1 True)
|
||||||
}
|
}
|
||||||
, setVolume { volume = v, muted = m }
|
, setVolume model.id { volume = v, muted = m }
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimationFrameDelta delta ->
|
AnimationFrameDelta delta ->
|
||||||
|
@ -261,76 +265,76 @@ update msg model =
|
||||||
( { model | showMiniature = miniature }, Cmd.none )
|
( { model | showMiniature = miniature }, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
port polymnyVideoInit : String -> Cmd msg
|
port polymnyVideoInit : ( String, String ) -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
init : String -> Cmd msg
|
init : String -> String -> Cmd msg
|
||||||
init =
|
init id url =
|
||||||
polymnyVideoInit
|
polymnyVideoInit ( id, url )
|
||||||
|
|
||||||
|
|
||||||
port polymnyVideoPlayPause : () -> Cmd msg
|
port polymnyVideoPlayPause : String -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
playPause : Cmd msg
|
playPause : String -> Cmd msg
|
||||||
playPause =
|
playPause =
|
||||||
polymnyVideoPlayPause ()
|
polymnyVideoPlayPause
|
||||||
|
|
||||||
|
|
||||||
port polymnyVideoSeek : Float -> Cmd msg
|
port polymnyVideoSeek : ( String, Float ) -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
seek : Float -> Cmd msg
|
seek : String -> Float -> Cmd msg
|
||||||
seek =
|
seek id s =
|
||||||
polymnyVideoSeek
|
polymnyVideoSeek ( id, s )
|
||||||
|
|
||||||
|
|
||||||
port polymnyVideoRequestFullscreen : () -> Cmd msg
|
port polymnyVideoRequestFullscreen : String -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
requestFullscreen : Cmd msg
|
requestFullscreen : String -> Cmd msg
|
||||||
requestFullscreen =
|
requestFullscreen =
|
||||||
polymnyVideoRequestFullscreen ()
|
polymnyVideoRequestFullscreen
|
||||||
|
|
||||||
|
|
||||||
port polymnyVideoExitFullscreen : () -> Cmd msg
|
port polymnyVideoExitFullscreen : String -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
exitFullscreen : Cmd msg
|
exitFullscreen : String -> Cmd msg
|
||||||
exitFullscreen =
|
exitFullscreen =
|
||||||
polymnyVideoExitFullscreen ()
|
polymnyVideoExitFullscreen
|
||||||
|
|
||||||
|
|
||||||
port polymnyVideoSetPlaybackRate : Float -> Cmd msg
|
port polymnyVideoSetPlaybackRate : ( String, Float ) -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
setPlaybackRate : Float -> Cmd msg
|
setPlaybackRate : String -> Float -> Cmd msg
|
||||||
setPlaybackRate =
|
setPlaybackRate id playbackRate =
|
||||||
polymnyVideoSetPlaybackRate
|
polymnyVideoSetPlaybackRate ( id, playbackRate )
|
||||||
|
|
||||||
|
|
||||||
port polymnyVideoSetQuality : Quality -> Cmd msg
|
port polymnyVideoSetQuality : ( String, Quality ) -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
setQuality : Quality -> Cmd msg
|
setQuality : String -> Quality -> Cmd msg
|
||||||
setQuality =
|
setQuality id quality =
|
||||||
polymnyVideoSetQuality
|
polymnyVideoSetQuality ( id, quality )
|
||||||
|
|
||||||
|
|
||||||
port polymnyVideoSetSubtitleTrack : Int -> Cmd msg
|
port polymnyVideoSetSubtitleTrack : ( String, Int ) -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
setSubtitleTrack : Int -> Cmd msg
|
setSubtitleTrack : String -> Int -> Cmd msg
|
||||||
setSubtitleTrack =
|
setSubtitleTrack id track =
|
||||||
polymnyVideoSetSubtitleTrack
|
polymnyVideoSetSubtitleTrack ( id, track )
|
||||||
|
|
||||||
|
|
||||||
port polymnyVideoSetVolume : { volume : Float, muted : Bool } -> Cmd msg
|
port polymnyVideoSetVolume : ( String, { volume : Float, muted : Bool } ) -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
setVolume : { volume : Float, muted : Bool } -> Cmd msg
|
setVolume : String -> { volume : Float, muted : Bool } -> Cmd msg
|
||||||
setVolume =
|
setVolume id volume =
|
||||||
polymnyVideoSetVolume
|
polymnyVideoSetVolume ( id, volume )
|
||||||
|
|
||||||
|
|
||||||
port polymnyVideoNowHasQualities : (List Int -> msg) -> Sub msg
|
port polymnyVideoNowHasQualities : (List Int -> msg) -> Sub msg
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
module Events exposing (player, seekBar, subs, video)
|
module Video.Events exposing (overlay, player, seekBar, subs, video)
|
||||||
|
|
||||||
import Browser.Events
|
import Browser.Events
|
||||||
import Element
|
import Element
|
||||||
|
@ -6,8 +6,8 @@ import Html
|
||||||
import Html.Attributes
|
import Html.Attributes
|
||||||
import Html.Events
|
import Html.Events
|
||||||
import Json.Decode as Decode
|
import Json.Decode as Decode
|
||||||
import Quality
|
|
||||||
import Video exposing (Video)
|
import Video exposing (Video)
|
||||||
|
import Video.Quality as Quality
|
||||||
|
|
||||||
|
|
||||||
subs : Video -> Sub Video.Msg
|
subs : Video -> Sub Video.Msg
|
||||||
|
@ -51,13 +51,12 @@ player =
|
||||||
List.map Element.htmlAttribute
|
List.map Element.htmlAttribute
|
||||||
[ Html.Events.on "fullscreenchange" decodeFullscreenChange
|
[ Html.Events.on "fullscreenchange" decodeFullscreenChange
|
||||||
, Html.Events.on "mousemove" (Decode.succeed Video.MouseMove)
|
, Html.Events.on "mousemove" (Decode.succeed Video.MouseMove)
|
||||||
, Html.Events.on "click" (Decode.succeed Video.PlayPause)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
video : List (Html.Attribute Video.Msg)
|
video : Video -> List (Html.Attribute Video.Msg)
|
||||||
video =
|
video model =
|
||||||
[ Html.Attributes.id "video"
|
[ Html.Attributes.id model.id
|
||||||
, Html.Events.on "playing" (Decode.succeed Video.NowPlaying)
|
, Html.Events.on "playing" (Decode.succeed Video.NowPlaying)
|
||||||
, Html.Events.on "pause" (Decode.succeed Video.NowPaused)
|
, Html.Events.on "pause" (Decode.succeed Video.NowPaused)
|
||||||
, Html.Events.on "durationchange" decodeDurationChanged
|
, Html.Events.on "durationchange" decodeDurationChanged
|
||||||
|
@ -69,6 +68,12 @@ video =
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
overlay : List (Element.Attribute Video.Msg)
|
||||||
|
overlay =
|
||||||
|
[ Element.htmlAttribute (Html.Events.on "click" (Decode.succeed Video.PlayPause))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
seekBar : Video -> List (Element.Attribute Video.Msg)
|
seekBar : Video -> List (Element.Attribute Video.Msg)
|
||||||
seekBar model =
|
seekBar model =
|
||||||
List.map Element.htmlAttribute
|
List.map Element.htmlAttribute
|
|
@ -1,4 +1,4 @@
|
||||||
module Icons exposing (..)
|
module Video.Icons exposing (..)
|
||||||
|
|
||||||
import Element exposing (Element)
|
import Element exposing (Element)
|
||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
|
@ -10,12 +10,14 @@ svgFeatherIcon : String -> List (Svg msg) -> Bool -> Element msg
|
||||||
svgFeatherIcon className lines f =
|
svgFeatherIcon className lines f =
|
||||||
Element.html
|
Element.html
|
||||||
(svg
|
(svg
|
||||||
[ if f then
|
[ class <| "feather feather-" ++ className
|
||||||
class <| "filled feather feather-" ++ className
|
, fill
|
||||||
|
(if f then
|
||||||
|
"currentColor"
|
||||||
|
|
||||||
else
|
else
|
||||||
class <| "feather feather-" ++ className
|
"none"
|
||||||
, fill "none"
|
)
|
||||||
, height "24"
|
, height "24"
|
||||||
, stroke "currentColor"
|
, stroke "currentColor"
|
||||||
, strokeLinecap "round"
|
, strokeLinecap "round"
|
|
@ -1,4 +1,4 @@
|
||||||
module Quality exposing (Quality, decode, isSameOption, toString)
|
module Video.Quality exposing (Quality, decode, isSameOption, toString)
|
||||||
|
|
||||||
import Json.Decode as Decode
|
import Json.Decode as Decode
|
||||||
|
|
|
@ -1,218 +1,38 @@
|
||||||
module Views exposing (..)
|
module Video.Views exposing (..)
|
||||||
|
|
||||||
import Element exposing (Element)
|
import Element exposing (Element)
|
||||||
import Element.Background as Background
|
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 as Events
|
|
||||||
import Html
|
import Html
|
||||||
import Html.Attributes
|
import Html.Attributes
|
||||||
import Icons
|
|
||||||
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 Video exposing (Video)
|
||||||
|
import Video.Events as Events
|
||||||
|
import Video.Icons as Icons
|
||||||
|
import Video.Quality as Quality
|
||||||
|
|
||||||
|
|
||||||
|
view : Video -> Element Video.Msg
|
||||||
|
view model =
|
||||||
|
Element.el
|
||||||
|
(Element.inFront (overlay model)
|
||||||
|
:: Element.inFront (menu model)
|
||||||
|
:: Element.width Element.fill
|
||||||
|
:: Element.height Element.fill
|
||||||
|
:: Background.color (Element.rgb 0 0 0)
|
||||||
|
:: Element.htmlAttribute (Html.Attributes.id (model.id ++ "-full"))
|
||||||
|
:: Events.player
|
||||||
|
)
|
||||||
|
(Element.html (Html.video (Html.Attributes.class "wf" :: Events.video model) []))
|
||||||
|
|
||||||
|
|
||||||
embed : ( Int, Int ) -> Video -> Element Video.Msg
|
embed : ( Int, Int ) -> Video -> Element Video.Msg
|
||||||
embed screenSize model =
|
embed screenSize model =
|
||||||
let
|
let
|
||||||
seen =
|
|
||||||
round (model.position * 1000)
|
|
||||||
|
|
||||||
loaded =
|
|
||||||
List.filter (\( start, end ) -> start < model.position) model.loaded
|
|
||||||
|
|
||||||
loadedToShow =
|
|
||||||
every model.duration loaded
|
|
||||||
|
|
||||||
showRange : ( Float, Float, Bool ) -> Element msg
|
|
||||||
showRange ( start, end, isLoaded ) =
|
|
||||||
let
|
|
||||||
portion =
|
|
||||||
round (1000 * (end - start))
|
|
||||||
in
|
|
||||||
Element.el
|
|
||||||
[ Element.width (Element.fillPortion portion)
|
|
||||||
, Element.height Element.fill
|
|
||||||
, if isLoaded then
|
|
||||||
Background.color (Element.rgba 1 1 1 0.5)
|
|
||||||
|
|
||||||
else
|
|
||||||
Background.color (Element.rgba 1 1 1 0)
|
|
||||||
]
|
|
||||||
Element.none
|
|
||||||
|
|
||||||
loadedElement =
|
|
||||||
Element.row
|
|
||||||
[ Element.width Element.fill
|
|
||||||
, Element.height (Element.px 5)
|
|
||||||
, Element.centerY
|
|
||||||
, Border.rounded 5
|
|
||||||
]
|
|
||||||
(List.map showRange loadedToShow)
|
|
||||||
|
|
||||||
remaining =
|
|
||||||
round ((model.duration - model.position) * 1000)
|
|
||||||
|
|
||||||
overlay =
|
|
||||||
Element.el
|
|
||||||
[ Element.width Element.fill
|
|
||||||
, Element.height Element.fill
|
|
||||||
, Font.color (Element.rgb 1 1 1)
|
|
||||||
, if not model.playing then
|
|
||||||
Background.color (Element.rgba 0 0 0 0.5)
|
|
||||||
|
|
||||||
else
|
|
||||||
Background.color (Element.rgba 0 0 0 0)
|
|
||||||
]
|
|
||||||
(case ( model.playing, model.showIcon ) of
|
|
||||||
( False, _ ) ->
|
|
||||||
Element.el
|
|
||||||
[ Element.centerX
|
|
||||||
, Element.centerY
|
|
||||||
, Element.scale 10
|
|
||||||
]
|
|
||||||
(Icons.play True)
|
|
||||||
|
|
||||||
( _, Just icon ) ->
|
|
||||||
animatedEl fadeOutZoom
|
|
||||||
[ Background.color (Element.rgb 0 0 0)
|
|
||||||
, Border.rounded 100
|
|
||||||
, Element.padding 10
|
|
||||||
, Element.centerX
|
|
||||||
, Element.centerY
|
|
||||||
]
|
|
||||||
icon
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
Element.none
|
|
||||||
)
|
|
||||||
|
|
||||||
bar =
|
|
||||||
animatedEl
|
|
||||||
(if model.animationFrame < 3000 then
|
|
||||||
fadeIn
|
|
||||||
|
|
||||||
else
|
|
||||||
fadeOut
|
|
||||||
)
|
|
||||||
[ Element.width Element.fill, Element.alignBottom ]
|
|
||||||
(Element.column
|
|
||||||
[ Element.width Element.fill
|
|
||||||
, Element.alignBottom
|
|
||||||
, Font.color (Element.rgba 1 1 1 0.85)
|
|
||||||
]
|
|
||||||
[ settings model
|
|
||||||
, Element.column
|
|
||||||
[ Element.width Element.fill
|
|
||||||
, Element.padding 10
|
|
||||||
, Background.gradient { angle = 0, steps = [ Element.rgba 0 0 0 0.75, Element.rgba 0 0 0 0 ] }
|
|
||||||
]
|
|
||||||
[ Element.row
|
|
||||||
[ Element.width Element.fill
|
|
||||||
, Element.height (Element.px 30)
|
|
||||||
, Border.rounded 5
|
|
||||||
, Element.behindContent
|
|
||||||
(Element.el
|
|
||||||
[ Background.color (Element.rgba 1 1 1 0.25)
|
|
||||||
, Element.width Element.fill
|
|
||||||
, Element.height (Element.px 5)
|
|
||||||
, Element.centerY
|
|
||||||
, Border.rounded 5
|
|
||||||
]
|
|
||||||
Element.none
|
|
||||||
)
|
|
||||||
, Element.behindContent loadedElement
|
|
||||||
, Element.inFront
|
|
||||||
(Element.el
|
|
||||||
(Element.width Element.fill
|
|
||||||
:: Element.height Element.fill
|
|
||||||
:: Element.pointer
|
|
||||||
:: Events.seekBar model
|
|
||||||
)
|
|
||||||
Element.none
|
|
||||||
)
|
|
||||||
, Element.above
|
|
||||||
(case model.showMiniature of
|
|
||||||
Just ( position, size ) ->
|
|
||||||
let
|
|
||||||
relativePosition =
|
|
||||||
toFloat position / toFloat size
|
|
||||||
|
|
||||||
percentage =
|
|
||||||
String.fromFloat (relativePosition * 100) ++ "%"
|
|
||||||
|
|
||||||
miniatureId =
|
|
||||||
round (relativePosition * 100)
|
|
||||||
|
|
||||||
miniatureIdString =
|
|
||||||
"miniature-" ++ String.padLeft 3 '0' (String.fromInt miniatureId) ++ ".png"
|
|
||||||
|
|
||||||
miniatureUrl =
|
|
||||||
model.url
|
|
||||||
|> String.split "/"
|
|
||||||
|> List.reverse
|
|
||||||
|> List.drop 1
|
|
||||||
|> (\list -> miniatureIdString :: list)
|
|
||||||
|> List.reverse
|
|
||||||
|> String.join "/"
|
|
||||||
|
|
||||||
rightPosition =
|
|
||||||
(position - 180 - 6)
|
|
||||||
|> max 0
|
|
||||||
|> min (size - 360 - 28)
|
|
||||||
|> toFloat
|
|
||||||
in
|
|
||||||
Element.column
|
|
||||||
[ Element.moveRight rightPosition
|
|
||||||
, Element.spacing 10
|
|
||||||
]
|
|
||||||
[ Element.image
|
|
||||||
[ Border.color (Element.rgb 1 1 1)
|
|
||||||
, Border.width 2
|
|
||||||
]
|
|
||||||
{ src = miniatureUrl, description = "miniature" }
|
|
||||||
, Element.el
|
|
||||||
[ Element.centerX
|
|
||||||
, Font.shadow
|
|
||||||
{ offset = ( 0, 0 )
|
|
||||||
, blur = 4
|
|
||||||
, color = Element.rgb 0 0 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
(Element.text (formatTime (relativePosition * model.duration)))
|
|
||||||
]
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
Element.none
|
|
||||||
)
|
|
||||||
]
|
|
||||||
[ Element.el
|
|
||||||
[ Background.color (Element.rgba 1 0 0 0.75)
|
|
||||||
, Element.width (Element.fillPortion seen)
|
|
||||||
, Element.height Element.fill
|
|
||||||
, Border.roundEach { topLeft = 5, topRight = 0, bottomLeft = 5, bottomRight = 0 }
|
|
||||||
, Element.height (Element.px 5)
|
|
||||||
, Element.centerY
|
|
||||||
]
|
|
||||||
Element.none
|
|
||||||
, Element.el [ Element.width (Element.fillPortion remaining) ] Element.none
|
|
||||||
]
|
|
||||||
, Element.row
|
|
||||||
[ Element.spacing 10, Element.width Element.fill ]
|
|
||||||
[ playPauseButton model.playing
|
|
||||||
, volumeButton model.volume model.muted
|
|
||||||
, Element.el [ Element.moveDown 2.5 ] (Element.text (formatTime model.position ++ " / " ++ formatTime model.duration))
|
|
||||||
, Element.row [ Element.spacing 10, Element.alignRight ]
|
|
||||||
[ settingsButton, fullscreenButton model.isFullscreen ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
videoAspectRatio =
|
videoAspectRatio =
|
||||||
toFloat (Tuple.first model.size) / toFloat (Tuple.second model.size)
|
toFloat (Tuple.first model.size) / toFloat (Tuple.second model.size)
|
||||||
|
|
||||||
|
@ -239,12 +59,12 @@ embed screenSize model =
|
||||||
)
|
)
|
||||||
in
|
in
|
||||||
Element.el
|
Element.el
|
||||||
(Element.inFront overlay
|
(Element.inFront (overlay model)
|
||||||
:: Element.inFront bar
|
:: Element.inFront (menu model)
|
||||||
:: Element.width Element.fill
|
:: Element.width Element.fill
|
||||||
:: Element.height Element.fill
|
:: Element.height Element.fill
|
||||||
:: Background.color (Element.rgb 0 0 0)
|
:: Background.color (Element.rgb 0 0 0)
|
||||||
:: Element.htmlAttribute (Html.Attributes.id "full")
|
:: Element.htmlAttribute (Html.Attributes.id (model.id ++ "-full"))
|
||||||
:: Events.player
|
:: Events.player
|
||||||
)
|
)
|
||||||
(Element.html
|
(Element.html
|
||||||
|
@ -254,7 +74,7 @@ embed screenSize model =
|
||||||
:: Html.Attributes.height h
|
:: Html.Attributes.height h
|
||||||
:: Html.Attributes.style "top" (String.fromInt y ++ "px")
|
:: Html.Attributes.style "top" (String.fromInt y ++ "px")
|
||||||
:: Html.Attributes.style "left" (String.fromInt x ++ "px")
|
:: Html.Attributes.style "left" (String.fromInt x ++ "px")
|
||||||
:: Events.video
|
:: Events.video model
|
||||||
)
|
)
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
@ -432,6 +252,215 @@ settings model =
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
overlay : Video -> Element Video.Msg
|
||||||
|
overlay model =
|
||||||
|
Element.el
|
||||||
|
(Element.width Element.fill
|
||||||
|
:: Element.height Element.fill
|
||||||
|
:: Font.color (Element.rgb 1 1 1)
|
||||||
|
:: (if not model.playing then
|
||||||
|
Background.color (Element.rgba 0 0 0 0.5)
|
||||||
|
|
||||||
|
else
|
||||||
|
Background.color (Element.rgba 0 0 0 0)
|
||||||
|
)
|
||||||
|
:: Events.overlay
|
||||||
|
)
|
||||||
|
(case ( model.playing, model.showIcon ) of
|
||||||
|
( False, _ ) ->
|
||||||
|
Element.el
|
||||||
|
[ Element.centerX
|
||||||
|
, Element.centerY
|
||||||
|
, Element.scale 10
|
||||||
|
]
|
||||||
|
(Icons.play True)
|
||||||
|
|
||||||
|
( _, Just icon ) ->
|
||||||
|
animatedEl fadeOutZoom
|
||||||
|
[ Background.color (Element.rgb 0 0 0)
|
||||||
|
, Border.rounded 100
|
||||||
|
, Element.padding 10
|
||||||
|
, Element.centerX
|
||||||
|
, Element.centerY
|
||||||
|
]
|
||||||
|
icon
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Element.none
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
menu : Video -> Element Video.Msg
|
||||||
|
menu model =
|
||||||
|
animatedEl
|
||||||
|
(if model.animationFrame < 3000 then
|
||||||
|
fadeIn
|
||||||
|
|
||||||
|
else
|
||||||
|
fadeOut
|
||||||
|
)
|
||||||
|
[ Element.width Element.fill, Element.alignBottom ]
|
||||||
|
(Element.column
|
||||||
|
[ Element.width Element.fill
|
||||||
|
, Element.alignBottom
|
||||||
|
, Font.color (Element.rgba 1 1 1 0.85)
|
||||||
|
]
|
||||||
|
[ settings model
|
||||||
|
, Element.column
|
||||||
|
[ Element.width Element.fill
|
||||||
|
, Element.padding 10
|
||||||
|
, Background.gradient { angle = 0, steps = [ Element.rgba 0 0 0 0.75, Element.rgba 0 0 0 0 ] }
|
||||||
|
]
|
||||||
|
[ seekbar model
|
||||||
|
, Element.row
|
||||||
|
[ Element.spacing 10, Element.width Element.fill ]
|
||||||
|
[ playPauseButton model.playing
|
||||||
|
, volumeButton model.volume model.muted
|
||||||
|
, Element.el [ Element.moveDown 2.5 ] (Element.text (formatTime model.position ++ " / " ++ formatTime model.duration))
|
||||||
|
, Element.row [ Element.spacing 10, Element.alignRight ]
|
||||||
|
[ settingsButton, fullscreenButton model.isFullscreen ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
seekbar : Video -> Element Video.Msg
|
||||||
|
seekbar model =
|
||||||
|
let
|
||||||
|
seen =
|
||||||
|
round (model.position * 1000)
|
||||||
|
|
||||||
|
loaded =
|
||||||
|
List.filter (\( start, end ) -> start < model.position) model.loaded
|
||||||
|
|
||||||
|
loadedToShow =
|
||||||
|
every model.duration loaded
|
||||||
|
|
||||||
|
showRange : ( Float, Float, Bool ) -> Element msg
|
||||||
|
showRange ( start, end, isLoaded ) =
|
||||||
|
let
|
||||||
|
portion =
|
||||||
|
round (1000 * (end - start))
|
||||||
|
in
|
||||||
|
Element.el
|
||||||
|
[ Element.width (Element.fillPortion portion)
|
||||||
|
, Element.height Element.fill
|
||||||
|
, if isLoaded then
|
||||||
|
Background.color (Element.rgba 1 1 1 0.5)
|
||||||
|
|
||||||
|
else
|
||||||
|
Background.color (Element.rgba 1 1 1 0)
|
||||||
|
]
|
||||||
|
Element.none
|
||||||
|
|
||||||
|
loadedElement =
|
||||||
|
Element.row
|
||||||
|
[ Element.width Element.fill
|
||||||
|
, Element.height (Element.px 5)
|
||||||
|
, Element.centerY
|
||||||
|
, Border.rounded 5
|
||||||
|
]
|
||||||
|
(List.map showRange loadedToShow)
|
||||||
|
|
||||||
|
remaining =
|
||||||
|
round ((model.duration - model.position) * 1000)
|
||||||
|
in
|
||||||
|
Element.row
|
||||||
|
[ Element.width Element.fill
|
||||||
|
, Element.height (Element.px 30)
|
||||||
|
, Border.rounded 5
|
||||||
|
, Element.behindContent
|
||||||
|
(Element.el
|
||||||
|
[ Background.color (Element.rgba 1 1 1 0.25)
|
||||||
|
, Element.width Element.fill
|
||||||
|
, Element.height (Element.px 5)
|
||||||
|
, Element.centerY
|
||||||
|
, Border.rounded 5
|
||||||
|
]
|
||||||
|
Element.none
|
||||||
|
)
|
||||||
|
, Element.behindContent loadedElement
|
||||||
|
, Element.inFront
|
||||||
|
(Element.el
|
||||||
|
(Element.width Element.fill
|
||||||
|
:: Element.height Element.fill
|
||||||
|
:: Element.pointer
|
||||||
|
:: Events.seekBar model
|
||||||
|
)
|
||||||
|
Element.none
|
||||||
|
)
|
||||||
|
, Element.above (miniature model)
|
||||||
|
]
|
||||||
|
[ Element.el
|
||||||
|
[ Background.color (Element.rgba 1 0 0 0.75)
|
||||||
|
, Element.width (Element.fillPortion seen)
|
||||||
|
, Element.height Element.fill
|
||||||
|
, Border.roundEach { topLeft = 5, topRight = 0, bottomLeft = 5, bottomRight = 0 }
|
||||||
|
, Element.height (Element.px 5)
|
||||||
|
, Element.centerY
|
||||||
|
]
|
||||||
|
Element.none
|
||||||
|
, Element.el [ Element.width (Element.fillPortion remaining) ] Element.none
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
miniature : Video -> Element Video.Msg
|
||||||
|
miniature model =
|
||||||
|
case model.showMiniature of
|
||||||
|
Just ( position, size ) ->
|
||||||
|
let
|
||||||
|
relativePosition =
|
||||||
|
toFloat position / toFloat size
|
||||||
|
|
||||||
|
percentage =
|
||||||
|
String.fromFloat (relativePosition * 100) ++ "%"
|
||||||
|
|
||||||
|
miniatureId =
|
||||||
|
round (relativePosition * 100)
|
||||||
|
|
||||||
|
miniatureIdString =
|
||||||
|
"miniature-" ++ String.padLeft 3 '0' (String.fromInt miniatureId) ++ ".png"
|
||||||
|
|
||||||
|
miniatureUrl =
|
||||||
|
model.url
|
||||||
|
|> String.split "/"
|
||||||
|
|> List.reverse
|
||||||
|
|> List.drop 1
|
||||||
|
|> (\list -> miniatureIdString :: list)
|
||||||
|
|> List.reverse
|
||||||
|
|> String.join "/"
|
||||||
|
|
||||||
|
rightPosition =
|
||||||
|
(position - 180 - 6)
|
||||||
|
|> max 0
|
||||||
|
|> min (size - 360 - 28)
|
||||||
|
|> toFloat
|
||||||
|
in
|
||||||
|
Element.column
|
||||||
|
[ Element.moveRight rightPosition
|
||||||
|
, Element.spacing 10
|
||||||
|
]
|
||||||
|
[ Element.image
|
||||||
|
[ Border.color (Element.rgb 1 1 1)
|
||||||
|
, Border.width 2
|
||||||
|
]
|
||||||
|
{ src = miniatureUrl, description = "miniature" }
|
||||||
|
, Element.el
|
||||||
|
[ Element.centerX
|
||||||
|
, Font.shadow
|
||||||
|
{ offset = ( 0, 0 )
|
||||||
|
, blur = 4
|
||||||
|
, color = Element.rgb 0 0 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
(Element.text (formatTime (relativePosition * model.duration)))
|
||||||
|
]
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Element.none
|
||||||
|
|
||||||
|
|
||||||
playPauseButton : Bool -> Element Video.Msg
|
playPauseButton : Bool -> Element Video.Msg
|
||||||
playPauseButton playing =
|
playPauseButton playing =
|
||||||
let
|
let
|
Loading…
Reference in New Issue