packages feed

movie-monad (empty) → 0.0.0.0

raw patch · 7 files changed

+677/−0 lines, 7 filesdep +MissingHdep +basedep +gi-gdkx11setup-changed

Dependencies added: MissingH, base, gi-gdkx11, gi-glib, gi-gobject, gi-gst, gi-gstvideo, gi-gtk, gi-xlib, haskell-gi-base, process, text

Files

+ LICENSE view
@@ -0,0 +1,29 @@+BSD 3-Clause License++Copyright (c) 2017, David Lettier+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+  list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+  this list of conditions and the following disclaimer in the documentation+  and/or other materials provided with the distribution.++* Neither the name of the copyright holder nor the names of its+  contributors may be used to endorse or promote products derived from+  this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,40 @@+![Movie Monad](https://i.imgur.com/Vt9Bipy.png)++# Movie Monad++A desktop video player built with Haskell that uses GStreamer and GTK+.++## Screenshots++<img src="http://i.imgur.com/QOMXSwo.jpg" width="600" alt="GUI" title= "GUI" />++## Documentation++[Let's make a GTK Video Player with Haskell](https://lettier.github.io/posts/2017-08-30-haskell-gtk-video-player.html)++## Install++### GitHub++```bash+# Install Git+# Install Haskell+# Install Haksell Stack+# Install ExifTool+git clone https://github.com/lettier/movie-monad.git+cd movie-monad/+stack setup+stack install --dependencies-only+haskell-gi -o lib/gi-gdkx11/GdkX11.overrides -O lib/gi-gdkx11 GdkX11-3.0+haskell-gi -o lib/gi-xlib/xlib.overrides     -O lib/gi-xlib xlib+stack build+stack install+stack exec -- movie-monad+```++## License++See [LICENSE](LICENSE).++(C) 2017 David Lettier  +[lettier.com](http://www.lettier.com/)
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ movie-monad.cabal view
@@ -0,0 +1,44 @@+name:                 movie-monad+version:              0.0.0.0+synopsis:             Plays videos using GStreamer and GTK+.+description:          Desktop video player that uses GStreamer and GTK+.+homepage:             https://github.com/lettier/movie-monad+license:              BSD3+license-file:         LICENSE+author:               Lettier+maintainer:           Lettier+copyright:            2017 David Lettier+category:               Multimedia+                      , Multimedia Player+                      , Video+build-type:           Simple+cabal-version:        >=1.10+extra-source-files:     README.md+                      , LICENSE+                      , ./src/dev/Paths_movie_monad.hs+                      , ./src/data/gui.glade+data-files:           data/gui.glade+data-dir:             ./src/++source-repository     head+  type:               git+  location:           https://github.com/lettier/gifcurry.git++executable            movie-monad+  main-is:              Main.hs+  default-language:     Haskell2010+  build-depends:        base >= 4.7 && < 5+                      , text == 1.2.*+                      , process == 1.4.*+                      , MissingH == 1.4.*+                      , haskell-gi-base == 0.20.*+                      , gi-gobject == 2.0.*+                      , gi-gst == 1.0.*+                      , gi-gtk == 3.0.*+                      , gi-glib == 2.0.*+                      , gi-xlib == 0.1.6.*+                      , gi-gstvideo == 1.0.*+                      , gi-gdkx11 == 0.3.*+  ghc-options:        -threaded -with-rtsopts=-N -Wall+  other-modules:      Paths_movie_monad+  hs-source-dirs:     ./src/
+ src/Main.hs view
@@ -0,0 +1,232 @@+{-+  Movie Monad+  (C) 2017 David lettier+  lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main where++import Prelude+import Foreign.C.Types+import System.Process+import System.Exit+import Control.Monad+import Control.Exception+import Data.Maybe+import Data.Int+import Data.Text+import Data.String.Utils+import Data.GI.Base+import Data.GI.Base.Properties+import GI.GLib+import GI.GObject+import qualified GI.Gtk+import GI.Gst+import GI.GstVideo+import GI.GdkX11+import Paths_movie_monad++-- Declare Element a type instance of IsVideoOverlay via a newtype wrapper+-- Our GStreamer element is playbin+-- Playbin implements the GStreamer VideoOverlay interface+newtype GstElement = GstElement GI.Gst.Element+instance GI.GstVideo.IsVideoOverlay GstElement++main :: IO ()+main = do+  _ <- GI.Gst.init Nothing+  _ <- GI.Gtk.init Nothing++  gladeFile <- getDataFileName "data/gui.glade"+  builder <- GI.Gtk.builderNewFromFile (pack gladeFile)++  window <- builderGetObject GI.Gtk.Window builder "window"+  fileChooserButton <- builderGetObject GI.Gtk.FileChooserButton builder "file-chooser-button"+  drawingArea <- builderGetObject GI.Gtk.Widget builder "drawing-area"+  seekScale <- builderGetObject GI.Gtk.Scale builder "seek-scale"+  onOffSwitch <- builderGetObject GI.Gtk.Switch builder "on-off-switch"+  volumeButton <- builderGetObject GI.Gtk.VolumeButton builder "volume-button"+  desiredVideoWidthComboBox <- builderGetObject GI.Gtk.ComboBoxText builder "desired-video-width-combo-box"+  errorMessageDialog <- builderGetObject GI.Gtk.MessageDialog builder "error-message-dialog"+  aboutButton <- builderGetObject GI.Gtk.Button builder "about-button"+  aboutDialog <- builderGetObject GI.Gtk.AboutDialog builder "about-dialog"++  playbin <- fromJust <$> GI.Gst.elementFactoryMake "playbin" (Just "MultimediaPlayer")++  _ <- GI.Gtk.onWidgetRealize drawingArea $ do+    gdkWindow <- fromJust <$> GI.Gtk.widgetGetWindow drawingArea+    x11Window <- GI.Gtk.unsafeCastTo GI.GdkX11.X11Window gdkWindow++    xid <- GI.GdkX11.x11WindowGetXid x11Window+    let xid' = fromIntegral xid :: CUIntPtr++    GI.GstVideo.videoOverlaySetWindowHandle (GstElement playbin) xid'++  _ <- GI.Gtk.onFileChooserButtonFileSet fileChooserButton $ do+    _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull++    filename <- fromJust <$> GI.Gtk.fileChooserGetFilename fileChooserButton+    let uri = "file://" ++ filename++    volume <- GI.Gtk.scaleButtonGetValue volumeButton+    Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume+    Data.GI.Base.Properties.setObjectPropertyString playbin "uri" (Just $ pack uri)++    desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox+    (success, width, height) <- getWindowSize desiredVideoWidth filename++    if success+      then do+        _ <- GI.Gst.elementSetState playbin GI.Gst.StatePlaying+        GI.Gtk.switchSetActive onOffSwitch True+        setWindowSize width height fileChooserButton drawingArea window+      else do+        _ <- GI.Gst.elementSetState playbin GI.Gst.StatePaused+        GI.Gtk.switchSetActive onOffSwitch False+        resetWindowSize desiredVideoWidth fileChooserButton drawingArea window+        _ <- GI.Gtk.onDialogResponse errorMessageDialog (\ _ -> GI.Gtk.widgetHide errorMessageDialog)+        void $ GI.Gtk.dialogRun errorMessageDialog++  _ <- GI.Gtk.onSwitchStateSet onOffSwitch $ \ switchOn -> do+    if switchOn+      then void $ GI.Gst.elementSetState playbin GI.Gst.StatePlaying+      else void $ GI.Gst.elementSetState playbin GI.Gst.StatePaused+    return switchOn++  _ <- GI.Gtk.onScaleButtonValueChanged volumeButton $+      \ volume -> void $ Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume++  seekScaleHandlerId <- GI.Gtk.onRangeValueChanged seekScale $ do+    (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime++    when couldQueryDuration $ do+      percentage' <- GI.Gtk.rangeGetValue seekScale+      let percentage = percentage' / 100.0+      let position = fromIntegral (round ((fromIntegral duration :: Double) * percentage) :: Int) :: Int64+      void $ GI.Gst.elementSeekSimple playbin GI.Gst.FormatTime [ GI.Gst.SeekFlagsFlush ] position++  _ <- GI.GLib.timeoutAddSeconds GI.GLib.PRIORITY_DEFAULT 1 $ do+    (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime+    (couldQueryPosition, position) <- GI.Gst.elementQueryPosition playbin GI.Gst.FormatTime++    let percentage =+          if couldQueryDuration && couldQueryPosition && duration > 0+            then 100.0 * (fromIntegral position / fromIntegral duration :: Double)+            else 0.0++    GI.GObject.signalHandlerBlock seekScale seekScaleHandlerId+    GI.Gtk.rangeSetValue seekScale percentage+    GI.GObject.signalHandlerUnblock seekScale seekScaleHandlerId++    return True++  _ <- GI.Gtk.onComboBoxChanged desiredVideoWidthComboBox $ do+    filename' <- GI.Gtk.fileChooserGetFilename fileChooserButton+    let filename = fromMaybe "" filename'++    desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox+    (success, width, height) <- getWindowSize desiredVideoWidth filename++    if success+      then setWindowSize width height fileChooserButton drawingArea window+      else resetWindowSize desiredVideoWidth fileChooserButton drawingArea window++  _ <- GI.Gtk.onWidgetButtonReleaseEvent aboutButton $ \ _ -> do+    _ <- GI.Gtk.onDialogResponse aboutDialog (\ _ -> GI.Gtk.widgetHide aboutDialog)+    void $ GI.Gtk.dialogRun aboutDialog+    return True++  _ <- GI.Gtk.onWidgetDestroy window $ do+    _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull+    _ <- GI.Gst.objectUnref playbin+    GI.Gtk.mainQuit++  GI.Gtk.widgetShowAll window+  GI.Gtk.main++builderGetObject ::+  (GI.GObject.GObject b, GI.Gtk.IsBuilder a) =>+  (Data.GI.Base.ManagedPtr b -> b) ->+  a ->+  Prelude.String ->+  IO b+builderGetObject objectTypeClass builder objectId =+  fromJust <$> GI.Gtk.builderGetObject builder (pack objectId) >>=+    GI.Gtk.unsafeCastTo objectTypeClass++getVideoInfo :: Prelude.String -> Prelude.String -> IO (Bool, Prelude.String)+getVideoInfo flag filename = do+  (code, out, _) <- catch (+      readProcessWithExitCode+        "exiftool"+        [flag, "-s", "-S", filename]+        ""+    ) (\ (_ :: Control.Exception.IOException) -> return (ExitFailure 1, "", ""))+  return (code == System.Exit.ExitSuccess, out)++isVideo :: Prelude.String -> IO Bool+isVideo filename = do+  (success, out) <- getVideoInfo "-MIMEType" filename+  return (success && isInfixOf "video" (pack out))++getWindowSize :: Int -> Prelude.String -> IO (Bool, Int32, Int32)+getWindowSize desiredVideoWidth filename = do+  let defaultWidth = 800+  let defaultHeight = 600++  video <- isVideo filename++  if video+    then do+      (success, out) <- getVideoInfo "-ImageSize" filename+      if success && isInfixOf "x" (pack out)+        then do+          let (width''':height''':_) =+                Data.String.Utils.split "x" $ Data.String.Utils.strip out++          let width'' = read width''' :: Int+          let height'' = read height''' :: Int++          let ratio = fromIntegral height'' / fromIntegral width'' :: Double+          let width' = fromIntegral desiredVideoWidth :: Double+          let height' = width' * ratio+          let width = fromIntegral (round width' :: Int) :: Int32+          let height = fromIntegral (round height' :: Int) :: Int32++          return (True, width, height)+        else return (False, defaultHeight, defaultWidth)+    else return (False, defaultHeight, defaultWidth)++getDesiredVideoWidth :: GI.Gtk.ComboBoxText -> IO Int+getDesiredVideoWidth = fmap (\ x -> read (Data.Text.unpack x) :: Int) . GI.Gtk.comboBoxTextGetActiveText++setWindowSize ::+  Int32 ->+  Int32 ->+  GI.Gtk.FileChooserButton ->+  GI.Gtk.Widget ->+  GI.Gtk.Window ->+  IO ()+setWindowSize width height fileChooserButton drawingArea window = do+  GI.Gtk.setWidgetWidthRequest fileChooserButton width++  GI.Gtk.setWidgetWidthRequest drawingArea width+  GI.Gtk.setWidgetHeightRequest drawingArea height++  GI.Gtk.setWidgetWidthRequest window width+  GI.Gtk.setWidgetHeightRequest window height+  GI.Gtk.windowResize window width (if height <= 0 then 1 else height)++resetWindowSize ::+  (Integral a) =>+  a ->+  GI.Gtk.FileChooserButton ->+  GI.Gtk.Widget ->+  GI.Gtk.Window ->+  IO ()+resetWindowSize width' fileChooserButton drawingArea window = do+  let width = fromIntegral width' :: Int32+  setWindowSize width 0 fileChooserButton drawingArea window
+ src/data/gui.glade view
@@ -0,0 +1,313 @@+<?xml version="1.0" encoding="UTF-8"?>+<!-- Generated with glade 3.20.0 -->+<interface>+  <requires lib="gtk+" version="3.20"/>+  <object class="GtkAdjustment" id="adjustment1">+    <property name="upper">100</property>+    <property name="step_increment">1</property>+    <property name="page_increment">10</property>+  </object>+  <object class="GtkWindow" id="window">+    <property name="can_focus">False</property>+    <property name="title" translatable="yes">Movie Monad</property>+    <property name="window_position">center</property>+    <property name="icon_name">applications-multimedia</property>+    <property name="gravity">center</property>+    <child>+      <object class="GtkBox">+        <property name="visible">True</property>+        <property name="can_focus">False</property>+        <property name="orientation">vertical</property>+        <child>+          <object class="GtkBox">+            <property name="visible">True</property>+            <property name="can_focus">False</property>+            <property name="orientation">vertical</property>+            <child>+              <object class="GtkFileChooserButton" id="file-chooser-button">+                <property name="width_request">800</property>+                <property name="visible">True</property>+                <property name="can_focus">False</property>+                <property name="tooltip_text" translatable="yes">Select a video file.</property>+                <property name="create_folders">False</property>+                <property name="title" translatable="yes">Movie Monad</property>+              </object>+              <packing>+                <property name="expand">False</property>+                <property name="fill">True</property>+                <property name="position">0</property>+              </packing>+            </child>+            <child>+              <object class="GtkDrawingArea" id="drawing-area">+                <property name="visible">True</property>+                <property name="sensitive">False</property>+                <property name="app_paintable">True</property>+                <property name="can_focus">False</property>+              </object>+              <packing>+                <property name="expand">False</property>+                <property name="fill">False</property>+                <property name="position">1</property>+              </packing>+            </child>+            <child>+              <object class="GtkScale" id="seek-scale">+                <property name="visible">True</property>+                <property name="can_focus">True</property>+                <property name="margin_left">5</property>+                <property name="margin_right">5</property>+                <property name="adjustment">adjustment1</property>+                <property name="show_fill_level">True</property>+                <property name="fill_level">100</property>+                <property name="round_digits">0</property>+                <property name="digits">-1</property>+                <property name="draw_value">False</property>+                <property name="value_pos">left</property>+              </object>+              <packing>+                <property name="expand">True</property>+                <property name="fill">True</property>+                <property name="position">2</property>+              </packing>+            </child>+          </object>+          <packing>+            <property name="expand">False</property>+            <property name="fill">True</property>+            <property name="position">0</property>+          </packing>+        </child>+        <child>+          <object class="GtkBox">+            <property name="visible">True</property>+            <property name="can_focus">False</property>+            <property name="margin_bottom">5</property>+            <child>+              <object class="GtkSwitch" id="on-off-switch">+                <property name="visible">True</property>+                <property name="can_focus">True</property>+                <property name="tooltip_text" translatable="yes">Toggle on to play. Toggle off to pause.</property>+                <property name="margin_left">5</property>+              </object>+              <packing>+                <property name="expand">False</property>+                <property name="fill">False</property>+                <property name="position">0</property>+              </packing>+            </child>+            <child>+              <object class="GtkVolumeButton" id="volume-button">+                <property name="visible">True</property>+                <property name="app_paintable">True</property>+                <property name="can_focus">True</property>+                <property name="focus_on_click">False</property>+                <property name="receives_default">True</property>+                <property name="double_buffered">False</property>+                <property name="margin_left">5</property>+                <property name="margin_right">5</property>+                <property name="relief">none</property>+                <property name="value">0.5</property>+                <property name="size">button</property>+                <property name="icons">audio-volume-muted-symbolic+audio-volume-high-symbolic+audio-volume-low-symbolic+audio-volume-medium-symbolic</property>+                <child internal-child="plus_button">+                  <object class="GtkButton">+                    <property name="can_focus">True</property>+                    <property name="receives_default">True</property>+                    <property name="halign">center</property>+                    <property name="valign">center</property>+                    <property name="relief">none</property>+                  </object>+                </child>+                <child internal-child="minus_button">+                  <object class="GtkButton">+                    <property name="can_focus">True</property>+                    <property name="receives_default">True</property>+                    <property name="halign">center</property>+                    <property name="valign">center</property>+                    <property name="relief">none</property>+                  </object>+                </child>+              </object>+              <packing>+                <property name="expand">False</property>+                <property name="fill">False</property>+                <property name="position">1</property>+              </packing>+            </child>+            <child>+              <object class="GtkComboBoxText" id="desired-video-width-combo-box">+                <property name="visible">True</property>+                <property name="can_focus">False</property>+                <property name="tooltip_text" translatable="yes">Set the max width.</property>+                <property name="active">1</property>+                <property name="has_frame">False</property>+                <property name="active_id">800</property>+                <items>+                  <item id="320" translatable="yes">320</item>+                  <item id="800" translatable="yes">800</item>+                  <item id="1024" translatable="yes">1024</item>+                  <item id="1152" translatable="yes">1152</item>+                  <item id="1280" translatable="yes">1280</item>+                  <item id="1360" translatable="yes">1360</item>+                  <item id="1366" translatable="yes">1366</item>+                  <item id="1440" translatable="yes">1440</item>+                  <item id="1536" translatable="yes">1536</item>+                  <item id="1600" translatable="yes">1600</item>+                  <item id="1680" translatable="yes">1680</item>+                  <item id="1920" translatable="yes">1920</item>+                  <item id="2560" translatable="yes">2560</item>+                  <item id="3440" translatable="yes">3440</item>+                  <item id="3840" translatable="yes">3840</item>+                </items>+              </object>+              <packing>+                <property name="expand">False</property>+                <property name="fill">False</property>+                <property name="position">2</property>+              </packing>+            </child>+            <child>+              <object class="GtkButton" id="about-button">+                <property name="label">gtk-about</property>+                <property name="visible">True</property>+                <property name="can_focus">True</property>+                <property name="receives_default">True</property>+                <property name="margin_right">5</property>+                <property name="use_stock">True</property>+                <property name="always_show_image">True</property>+              </object>+              <packing>+                <property name="expand">False</property>+                <property name="fill">False</property>+                <property name="pack_type">end</property>+                <property name="position">3</property>+              </packing>+            </child>+          </object>+          <packing>+            <property name="expand">False</property>+            <property name="fill">True</property>+            <property name="position">1</property>+          </packing>+        </child>+      </object>+    </child>+    <child type="titlebar">+      <placeholder/>+    </child>+  </object>+  <object class="GtkAboutDialog" id="about-dialog">+    <property name="can_focus">False</property>+    <property name="title" translatable="yes">Movie Monad</property>+    <property name="window_position">center</property>+    <property name="icon_name">dialog-information</property>+    <property name="type_hint">dialog</property>+    <property name="gravity">center</property>+    <property name="transient_for">window</property>+    <property name="attached_to">window</property>+    <property name="program_name">Movie Monad</property>+    <property name="version">0.0.0.0</property>+    <property name="copyright" translatable="yes">(C) 2017 David Lettier</property>+    <property name="website">https://github.com/lettier/movie-monad</property>+    <property name="website_label" translatable="yes">Update</property>+    <property name="license">BSD 3-Clause License++Copyright (c) 2017, David Lettier+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+  list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+  this list of conditions and the following disclaimer in the documentation+  and/or other materials provided with the distribution.++* Neither the name of the copyright holder nor the names of its+  contributors may be used to endorse or promote products derived from+  this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</property>+    <property name="authors">David Lettier</property>+    <property name="logo_icon_name">applications-multimedia</property>+    <property name="license_type">custom</property>+    <child internal-child="vbox">+      <object class="GtkBox">+        <property name="can_focus">False</property>+        <property name="orientation">vertical</property>+        <property name="spacing">2</property>+        <child internal-child="action_area">+          <object class="GtkButtonBox">+            <property name="can_focus">False</property>+            <property name="layout_style">end</property>+          </object>+          <packing>+            <property name="expand">False</property>+            <property name="fill">False</property>+            <property name="position">0</property>+          </packing>+        </child>+        <child>+          <placeholder/>+        </child>+      </object>+    </child>+    <child>+      <placeholder/>+    </child>+  </object>+  <object class="GtkMessageDialog" id="error-message-dialog">+    <property name="can_focus">False</property>+    <property name="title" translatable="yes">Movie Monad</property>+    <property name="window_position">center</property>+    <property name="destroy_with_parent">True</property>+    <property name="icon_name">dialog-error</property>+    <property name="type_hint">dialog</property>+    <property name="urgency_hint">True</property>+    <property name="gravity">center</property>+    <property name="transient_for">window</property>+    <property name="attached_to">window</property>+    <property name="message_type">error</property>+    <property name="buttons">close</property>+    <property name="text" translatable="yes">There was an error.</property>+    <property name="secondary_text" translatable="yes">Did you select a video file?+Did you install ExifTool?</property>+    <child internal-child="vbox">+      <object class="GtkBox">+        <property name="can_focus">False</property>+        <property name="orientation">vertical</property>+        <property name="spacing">2</property>+        <child internal-child="action_area">+          <object class="GtkButtonBox">+            <property name="can_focus">False</property>+            <property name="homogeneous">True</property>+            <property name="layout_style">expand</property>+          </object>+          <packing>+            <property name="expand">False</property>+            <property name="fill">False</property>+            <property name="position">0</property>+          </packing>+        </child>+      </object>+    </child>+    <child>+      <placeholder/>+    </child>+  </object>+</interface>
+ src/dev/Paths_movie_monad.hs view
@@ -0,0 +1,17 @@+{-+  Movie Monad+  (C) 2017 David lettier+  lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}++module Paths_movie_monad where++dataDir :: String+dataDir = "./src/"++getDataFileName :: FilePath -> IO FilePath+getDataFileName a = do+  putStrLn "You are using a fake Paths_movie_monad."+  return (dataDir ++ "/" ++ a)