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 +29/−0
- README.md +40/−0
- Setup.hs +2/−0
- movie-monad.cabal +44/−0
- src/Main.hs +232/−0
- src/data/gui.glade +313/−0
- src/dev/Paths_movie_monad.hs +17/−0
+ 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++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)