packages feed

miso-1.12.0.0: src/Miso/Navigator.hs

-----------------------------------------------------------------------------
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE CPP                 #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Miso.Navigator
-- Copyright   :  (C) 2016-2026 David M. Johnson
-- License     :  BSD3-style (see the file LICENSE)
-- Maintainer  :  David M. Johnson <code@dmj.io>
-- Stability   :  experimental
-- Portability :  non-portable
--
-- = Overview
--
-- "Miso.Navigator" wraps the browser's
-- <https://developer.mozilla.org/en-US/docs/Web/API/Navigator Navigator>
-- API as 'Miso.Effect.Effect' combinators that integrate directly into the
-- Model-View-Update loop. Each function returns an 'Miso.Effect.Effect' and
-- feeds its result back as an action via 'Miso.Effect.withSink' or
-- 'Miso.Effect.io'.
--
-- = Quick start
--
-- @
-- import "Miso"
-- import "Miso.Navigator"
--
-- data Action
--   = RequestLocation
--   | GotLocation 'Geolocation'
--   | LocationError 'GeolocationError'
--   | RequestCamera
--   | GotStream 'Stream'
--   | MediaError 'Miso.DSL.JSVal'
--
-- update :: Action -> 'Miso.Effect.Effect' p props Model Action
-- update RequestLocation =
--   'geolocation' GotLocation LocationError
-- update RequestCamera =
--   'getUserMedia' ('userMedia' { audio = False }) GotStream MediaError
-- update _ = pure ()
-- @
--
-- = API groups
--
-- * __Camera \/ microphone__ ('Navigator.mediaDevices.getUserMedia'):
--   'getUserMedia', 'userMedia', 'UserMedia', 'Stream'
-- * __Clipboard__ ('Navigator.clipboard.writeText'):
--   'copyClipboard'
-- * __Online status__ ('Navigator.onLine'):
--   'isOnLine'
-- * __Geolocation__ ('Navigator.geolocation.getCurrentPosition'):
--   'geolocation', 'Geolocation', 'GeolocationError', 'GeolocationErrorCode'
--
-- = Error handling
--
-- Geolocation errors are decoded from the browser's
-- <https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError GeolocationPositionError>
-- object into 'GeolocationError', which carries a 'GeolocationErrorCode'
-- (@'PERMISSION_DENIED'@, @'POSITION_UNAVAILABLE'@, @'TIMEOUT'@) and a
-- human-readable message string.
--
-- = See also
--
-- * "Miso.FFI.Internal" — 'Miso.FFI.Internal.getUserMedia', 'Miso.FFI.Internal.copyClipboard',
--   'Miso.FFI.Internal.geolocation', 'Miso.FFI.Internal.isOnLine' — the raw IO primitives
-- * "Miso.Subscription.OnLine" — subscription-based online\/offline monitoring
-- * "Miso.Effect" — 'Miso.Effect.withSink', 'Miso.Effect.io'
-----------------------------------------------------------------------------
module Miso.Navigator
  ( -- ** User media
    getUserMedia
  , userMedia
  , UserMedia (..)
  , Stream
  -- ** Clipboard
  , copyClipboard
  -- ** OnLine
  , isOnLine
  -- ** Geolocation
  , geolocation
  , Geolocation (..)
  , GeolocationError (..)
  , GeolocationErrorCode (..)
  ) where
-----------------------------------------------------------------------------
import           Control.Monad ((<=<))
import           Prelude hiding ((!!))
-----------------------------------------------------------------------------
import           Miso.DSL
import           Miso.String
import           Miso.Effect
import qualified Miso.FFI.Internal as FFI
----------------------------------------------------------------------------
-- | A media stream
type Stream = JSVal
----------------------------------------------------------------------------
-- | Get access to user's media devices.
--
-- <https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia>
--
getUserMedia
  :: UserMedia
  -- ^ Options
  -> (Stream -> action)
  -- ^ Successful callback
  -> (JSVal -> action)
  -- ^ Errorful callback
  -> Effect parent props model action
getUserMedia UserMedia {..} successful errorful =
  withSink $ \sink ->
    FFI.getUserMedia audio video
      (sink . successful)
      (sink . errorful)
-----------------------------------------------------------------------------
-- | Get access to the user's clipboard.
--
-- <https://developer.mozilla.org/en-US/docs/Web/API/Navigator/clipboard>
--
copyClipboard
  :: MisoString
  -- ^ Options
  -> action
  -- ^ Successful callback
  -> (JSVal -> action)
  -- ^ Errorful callback
  -> Effect parent props model action
copyClipboard txt successful errorful =
  withSink $ \sink ->
    FFI.copyClipboard txt
      (sink successful)
      (sink . errorful)
-----------------------------------------------------------------------------
-- | Get user's online status
--
-- <https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine>
--
isOnLine
  :: (Bool -> action)
  -- ^ Successful callback
  -> Effect parent props model action
isOnLine action = io (action <$> FFI.isOnLine)
-----------------------------------------------------------------------------
-- | Type for dealing with 'navigator.mediaDevices.getUserMedia'
--
-- <https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mediaDevices>
--
data UserMedia
  = UserMedia
  { audio :: Bool
  -- ^ Request access to the user's microphone
  , video :: Bool
  -- ^ Request access to the user's camera
  } deriving (Show, Eq)
-----------------------------------------------------------------------------
-- | Default t'UserMedia'
userMedia :: UserMedia
userMedia = UserMedia True True
-----------------------------------------------------------------------------
-- | Geolocation fetching
--
-- <https://developer.mozilla.org/en-US/docs/Web/API/Navigator/geolocation>
--
geolocation
  :: (Geolocation -> action)
  -- ^ Success callback; receives the device's current position
  -> (GeolocationError -> action)
  -- ^ Error callback; receives a 'GeolocationError' with code and message
  -> Effect parent props model action
geolocation successful errorful = do
  withSink $ \sink ->
    FFI.geolocation
      (sink . successful <=< fromJSValUnchecked)
      (sink . errorful <=< fromJSValUnchecked)
-----------------------------------------------------------------------------
-- | Geolocation errors
data GeolocationError = GeolocationError GeolocationErrorCode MisoString
  deriving (Show, Eq)
-----------------------------------------------------------------------------
instance FromJSVal GeolocationError where
  fromJSVal v = do
    code <- fromJSVal =<< (v ! "code")
    msg <- fromJSVal =<< (v ! "message")
    pure (GeolocationError <$> code <*> msg)
-----------------------------------------------------------------------------
-- | Geolocation error code
data GeolocationErrorCode
  = PERMISSION_DENIED
  | POSITION_UNAVAILABLE
  | TIMEOUT
  deriving (Enum, Show, Eq)
-----------------------------------------------------------------------------
instance FromJSVal GeolocationErrorCode where
  fromJSVal code =
    fromJSValUnchecked code >>= \case
      (1 :: Int) -> pure (Just PERMISSION_DENIED)
      2 -> pure (Just POSITION_UNAVAILABLE)
      3 -> pure (Just TIMEOUT)
      _ -> pure Nothing
-----------------------------------------------------------------------------
-- | Geolocation holds latitude, longitude and accuracy, among others.
data Geolocation
  = Geolocation
  { latitude :: Double
  -- ^ Latitude in decimal degrees
  , longitude :: Double
  -- ^ Longitude in decimal degrees
  , accuracy :: Double
  -- ^ Accuracy of the position in metres (95% confidence radius)
  } deriving (Show, Eq)
-----------------------------------------------------------------------------
instance FromJSVal Geolocation where
  fromJSVal geo = do
    lat <- fromJSVal =<< geo ! "coords" ! "latitude"
    lon <- fromJSVal =<< geo ! "coords" ! "longitude"
    acc <- fromJSVal =<< geo ! "coords" ! "accuracy"
    pure (Geolocation <$> lat <*> lon <*> acc)
-----------------------------------------------------------------------------