packages feed

monomer-flatpak-example-0.0.13.0: app/Camera.hs

module Camera (camera) where

import Control.Exception (SomeException, catch)
import Data.Function ((&))
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text, intercalate, pack)
import Desktop.Portal (Client)
import Desktop.Portal qualified as Portal
import Monomer
import PipeWire (PipeWireCallbacks (..), PipeWireClient, RegistryObject (..))
import PipeWire qualified
import System.Posix.Types (Fd)

data CameraModel = CameraModel
  { portalClient :: Client,
    state :: CameraState
  }
  deriving (Eq, Show)

data CameraState
  = StateCheckingPresent
  | StateNotFound
  | StateAccessing
  | StateAccessDenied
  | StateOpeningPipeWire
  | StateOpenedPipeWire Fd
  | StateConnectedToPipewire PipeWireClient (Map Word RegistryObject)
  deriving (Eq, Show)

data CameraEvent
  = Init
  | Dispose
  | SetState CameraState
  | ShowAlert {title :: Text, body :: Text}
  | CamerasChanged (Map Word RegistryObject)

camera :: (CompParentModel s, CompositeEvent e) => Client -> (Text -> Text -> e) -> WidgetNode s e
camera portalClient parentAlert =
  compositeD_ "MonomerFlatpakExample.Camera" (WidgetValue initialModel) buildUI (handleEvent parentAlert) cfg
  where
    cfg = [onInit Init, onDispose Dispose]
    initialModel = CameraModel {portalClient, state = StateCheckingPresent}

buildUI :: UIBuilder CameraModel CameraEvent
buildUI _wenv model =
  label_ (lbl model.state) [multiline] `styleBasic` [padding 40]
  where
    lbl = \case
      StateCheckingPresent ->
        "Checking for camera..."
      StateAccessing ->
        "Requesting access to camera..."
      StateNotFound ->
        "Camera not found."
      StateAccessDenied ->
        "Camera access denied."
      StateOpeningPipeWire ->
        "Opening pipewire..."
      StateOpenedPipeWire fd ->
        "Opened pipewire: " <> pack (show fd)
      StateConnectedToPipewire _client cameras ->
        "Found cameras:\n\n" <> formatCameras cameras

formatCameras :: Map Word RegistryObject -> Text
formatCameras cameras =
  (formatCamera <$> Map.elems cameras)
    & intercalate "\n\n"
  where
    formatCamera c =
      Map.assocs c.props.items
        & fmap (\(key, value) -> key <> ": " <> value)
        & intercalate "\n"

handleEvent :: (Text -> Text -> ep) -> EventHandler CameraModel CameraEvent sp ep
handleEvent parentAlert _env _node model = \case
  Init ->
    [ Producer $ \emit -> do
        catchErrors "Checking for camera failed" emit $ do
          Portal.isCameraPresent model.portalClient >>= \case
            False -> emit (SetState StateNotFound)
            True -> do
              emit (SetState StateAccessing)
              Portal.accessCamera model.portalClient >>= Portal.await >>= \case
                Nothing -> emit (SetState StateAccessDenied)
                Just () -> do
                  emit (SetState StateOpeningPipeWire)
                  fd <- Portal.openPipeWireRemote model.portalClient
                  emit (SetState (StateOpenedPipeWire fd))
                  let cbs =
                        PipeWireCallbacks
                          { cameraAdded = \cameras _ -> emit (CamerasChanged cameras),
                            cameraRemoved = \cameras _ -> emit (CamerasChanged cameras)
                          }
                  pwClient <- PipeWire.run fd cbs
                  emit (SetState (StateConnectedToPipewire pwClient mempty))
                  -- need to keep thread alive so monomer keeps accepting our events!
                  PipeWire.waitForQuit pwClient
    ]
  Dispose ->
    [ Producer $ \_emit -> do
        case model.state of
          StateConnectedToPipewire pwClient _ ->
            PipeWire.quit pwClient
          _ ->
            pure ()
    ]
  SetState state ->
    [Model model {state}]
  ShowAlert {title, body} ->
    [Report (parentAlert title body)]
  CamerasChanged cameras ->
    case model.state of
      StateConnectedToPipewire client _cameras ->
        [Model model {state = StateConnectedToPipewire client cameras}]
      _ -> []

catchErrors :: Text -> (CameraEvent -> IO ()) -> IO () -> IO ()
catchErrors title emit cmd = catch cmd handler
  where
    handler (e :: SomeException) =
      emit (ShowAlert title (pack (show e)))