diff --git a/index.html b/index.html index f8fbaa7..3091410 100644 --- a/index.html +++ b/index.html @@ -79,6 +79,7 @@ app.ports.seek.subscribe(function(arg) { const video = document.getElementById('video'); + console.log(arg); video.currentTime = arg; }); @@ -113,6 +114,12 @@ }); } }); + + app.ports.setVolume.subscribe(function(arg) { + const video = document.getElementById('video'); + video.volume = arg.volume; + video.muted = arg.muted; + }); diff --git a/src/Icons.elm b/src/Icons.elm index 6dc6793..e87a38d 100644 --- a/src/Icons.elm +++ b/src/Icons.elm @@ -78,3 +78,35 @@ check = svgFeatherIcon "check" [ Svg.polyline [ points "20 6 9 17 4 12" ] [] ] + + +volume : Bool -> Element msg +volume = + svgFeatherIcon "volume" + [ Svg.polygon [ points "11 5 6 9 2 9 2 15 6 15 11 19 11 5" ] [] + ] + + +volume1 : Bool -> Element msg +volume1 = + svgFeatherIcon "volume-1" + [ Svg.polygon [ points "11 5 6 9 2 9 2 15 6 15 11 19 11 5" ] [] + , Svg.path [ d "M15.54 8.46a5 5 0 0 1 0 7.07" ] [] + ] + + +volume2 : Bool -> Element msg +volume2 = + svgFeatherIcon "volume-2" + [ Svg.polygon [ points "11 5 6 9 2 9 2 15 6 15 11 19 11 5" ] [] + , Svg.path [ d "M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07" ] [] + ] + + +volumeX : Bool -> Element msg +volumeX = + svgFeatherIcon "volume-x" + [ Svg.polygon [ points "11 5 6 9 2 9 2 15 6 15 11 19 11 5" ] [] + , Svg.line [ x1 "23", y1 "9", x2 "17", y2 "15" ] [] + , Svg.line [ x1 "17", y1 "9", x2 "23", y2 "15" ] [] + ] diff --git a/src/Main.elm b/src/Main.elm index 3fa39f5..3b808ac 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -26,7 +26,7 @@ main = , update = update , view = view , subscriptions = - \_ -> + \model -> Sub.batch [ nowHasQualities NowHasQualities , nowHasQuality @@ -40,6 +40,7 @@ main = ) , Browser.Events.onAnimationFrameDelta AnimationFrameDelta , Browser.Events.onResize (\x y -> NowHasWindowSize ( x, y )) + , Browser.Events.onKeyDown (decodeKeyDown model) ] , onUrlChange = \_ -> Noop , onUrlRequest = \_ -> Noop @@ -81,6 +82,7 @@ type Msg | SetSettings Settings | SetPlaybackRate Float | SetQuality Quality.Quality + | SetVolume Float Bool | RequestFullscreen | ExitFullscreen | AnimationFrameDelta Float @@ -145,8 +147,8 @@ update msg model = PlayPause -> ( model, playPause () ) - Seek ratio -> - ( model, seek (ratio * model.duration) ) + Seek time -> + ( model, seek time ) SetPlaybackRate rate -> ( { model | showSettings = False, settings = All }, setPlaybackRate rate ) @@ -166,6 +168,9 @@ update msg model = 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 ) @@ -301,7 +306,7 @@ video model = (Element.width Element.fill :: Element.height Element.fill :: Element.pointer - :: seekBarEvents + :: seekBarEvents model ) Element.none ) @@ -320,6 +325,7 @@ video 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 ] @@ -534,6 +540,28 @@ fullscreenButton isFullscreen = ) +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 [] @@ -564,10 +592,10 @@ videoEvents = ] -seekBarEvents : List (Element.Attribute Msg) -seekBarEvents = +seekBarEvents : Model -> List (Element.Attribute Msg) +seekBarEvents model = List.map Element.htmlAttribute - [ Html.Events.on "click" decodeSeek + [ Html.Events.on "click" (decodeSeek model) ] @@ -593,9 +621,9 @@ decodeVolumeChange = (Decode.field "muted" Decode.bool) -decodeSeek : Decode.Decoder Msg -decodeSeek = - Decode.map2 (\x y -> Seek (toFloat x / toFloat y)) +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) @@ -646,6 +674,63 @@ decodePlaybackRateChange = (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) @@ -724,6 +809,9 @@ 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