movie-monad 0.0.1.0 → 0.0.2.0
raw patch · 25 files changed
+2994/−524 lines, 25 filesdep +filepathdep +gi-gdkpixbufdep +network-uridep −gi-gdkx11dep −gi-xlibdep ~gi-gtkdep ~haskell-gi-basebinary-added
Dependencies added: filepath, gi-gdkpixbuf, network-uri, system-fileio, system-filepath, time
Dependencies removed: gi-gdkx11, gi-xlib
Dependency ranges changed: gi-gtk, haskell-gi-base
Files
- CHANGELOG.md +84/−0
- README.md +142/−15
- movie-monad.cabal +58/−12
- src/About.hs +39/−0
- src/CommandLine.hs +32/−0
- src/Constants.hs +17/−0
- src/ErrorMessage.hs +42/−0
- src/FileChooser.hs +190/−0
- src/Fullscreen.hs +53/−0
- src/Keyboard.hs +74/−0
- src/Main.hs +112/−324
- src/Mouse.hs +69/−0
- src/PlayPause.hs +81/−0
- src/Playbin.hs +122/−0
- src/Records.hs +76/−0
- src/ScreensaverAndPowerManagement.hs +108/−0
- src/Seek.hs +93/−0
- src/Uri.hs +66/−0
- src/Utils.hs +58/−0
- src/VideoInfo.hs +76/−0
- src/VideoSizeSelector.hs +54/−0
- src/Window.hs +195/−0
- src/data/gui.glade +385/−173
- src/data/movie-monad-icon.png binary
- src/data/movie-monad-logo.svg +768/−0
+ CHANGELOG.md view
@@ -0,0 +1,84 @@+# [Movie Monad](https://github.com/lettier/movie-monad)++## Changelog++### 0.0.2.0++#### Added++- Hides the cursor and the on-screen controls when the mouse is idle+- New Logo+- URL support+- Keyboard shortcuts+- makefile+- Icon+- AppImage+- AppImage installation script+- Support for GTK 3.10+- macOS screenshots+- macOS build instructions+- Screensaver and power management enable/disable+- Video file path command line option that, if present, plays the video on window realized++#### Changed++- Cabal file+ - Corrected source-repository+- Logo+- README+- Dependencies+- Icon+- Play/Pause toggle to button+- ICO file to PNG+- Switched from GstVideoOverlay to GtkSink++#### Removed++- ExifTool requirement+- Old logo PNG file+- X11 dependency++-------------------------------------------------------------------------------++### 0.0.1.0++#### Added++- Fullscreen mode++#### Changed++- Screenshot+- Main function+ - Extracted callback functions into their own functions+- Dependencies to match Hackage++#### Removed++- `lib` directory++-------------------------------------------------------------------------------++### 0.0.0.0++#### Added++- File Loader+- Seek+- Pause+- Play+- Window Width Selector+- About+- Screenshot+- Logo+- README+- LICENSE+- Documentation++#### Changed++-++#### Removed++-
README.md view
@@ -1,4 +1,4 @@-+ # Movie Monad @@ -6,30 +6,157 @@ ## Screenshots -+ -## Documentation+ -[Let's make a GTK Video Player with Haskell](https://lettier.github.io/posts/2017-08-30-haskell-gtk-video-player.html)+ -## Install+## Features -### GitHub+* Local or remote video file playback+ * `file://`+ * `http://`+ * `https://`+* Window size selection+* Fullscreen mode+* Seek+* Play and pause+* Volume+* Command line play+ * `movie-monad ./path/to/video/file.mp4`+ * `movie-monad http://www.domain.tld/path/to/video/file.mp4` +## Keyboard Shortcuts++* Mute/Unmute+ * `m`+ * `AudioMute`+* Fullscreen/Windowed+ * `f`+* Show Controls+ * `c`+* Volume Up+ * `ArrowUp`+ * `AudioRaiseVolume`+* Volume Down+ * `ArrowDown`+ * `AudioLowerVolume`+* Play/Pause+ * `Space`+ * `AudioPlay`++## Tested On++### Linux++* [Ubuntu 17.10](https://www.ubuntu.com/desktop)+* [Ubuntu 16.04](https://www.ubuntu.com/desktop)+* [Ubuntu 14.04](https://www.ubuntu.com/desktop)+* [Linux Mint 18.2](https://linuxmint.com/)+* [Manjaro 17.04](https://manjaro.org/)+* [Deepin 15.4.1](https://www.deepin.org/en/dde/)+* [Antergos 17.10](https://antergos.com/)++### Mac++* [macOS Sierra 10.12.6](https://en.wikipedia.org/wiki/MacOS_Sierra)++## Dependencies++### Run++* [GTK+ >= 3.10](https://www.gtk.org/download/index.php)+* [GStreamer >= 1.0](https://gstreamer.freedesktop.org/download/)+ * [GStreamer Good Plug-ins](https://gstreamer.freedesktop.org/modules/gst-plugins-good.html)+ * [GStreamer Bad Plug-ins](https://gstreamer.freedesktop.org/modules/gst-plugins-bad.html)++### Build++* [Haskell](https://www.haskell.org/platform/prior.html)+ * GHC == 8.0.2++## Install & Run++### Linux++#### AppImage+ ```bash-# Install Git-# Install XQuartz (https://www.xquartz.org/) if using Mac OSX or macOS-# Install GTK+ 3.* (https://www.gtk.org/download/index.php)-# Install Haskell-# Install Haksell Stack-# Install ExifTool+# Install GTK+ >= 3.10 (https://www.gtk.org/download/index.php)+# Install GStreamer >= 1.0 (https://gstreamer.freedesktop.org/download/)+# Install GStreamer Good Plug-ins (https://gstreamer.freedesktop.org/modules/gst-plugins-good.html)+# Install GStreamer Bad Plug-ins >= 1.8 (https://gstreamer.freedesktop.org/modules/gst-plugins-bad.html)+# Visit https://github.com/lettier/movie-monad/releases+# * equals the current version+# Download the latest AppImage movie-monad-*-x86_64.AppImage+wget https://github.com/lettier/movie-monad/releases/download/*/movie-monad-*-x86_64.AppImage+chmod a+x movie-monad-*-x86_64.AppImage+./movie-monad-*-x86_64.AppImage+# If you would like to install the AppImage, you can run the following+cd+wget "https://raw.githubusercontent.com/lettier/movie-monad/master/packaging/linux/movie-monad-install.sh" -O "movie-monad-install.sh"+chmod +x "movie-monad-install.sh"+./movie-monad-install.sh+```++### Mac++```bash+# Install Homebrew (https://brew.sh/#install)+cd ~/Downloads+brew update && brew upgrade+brew install git git clone https://github.com/lettier/movie-monad.git-cd movie-monad/+cd movie-monad+brew cask install haskell-platform+brew install pkg-config libffi libav libsvg librsvg libogg libvorbis openh264 theora \+ gobject-introspection cairo gdk-pixbuf gsettings-desktop-schemas gtk+3 gtk-mac-integration \+ gstreamer gst-libav gst-plugins-base gst-plugins-good+brew install --with-gtk+3 gst-plugins-bad stack setup+stack install hsc2hs stack install-stack exec -- movie-monad-# Or just `movie-monad` if `stack path | grep local-bin-path` is in your `echo $PATH`+export PATH=$PATH:"$HOME/.local/bin/"+movie-monad ```++### [Hackage](https://hackage.haskell.org/package/movie-monad)++```bash+# Install GObject Introspection (https://wiki.gnome.org/action/show/Projects/GObjectIntrospection)+# Install GTK+ >= 3.10 (https://www.gtk.org/download/index.php)+# Install GStreamer >= 1.0 (https://gstreamer.freedesktop.org/download/)+# Install GStreamer Good Plug-ins (https://gstreamer.freedesktop.org/modules/gst-plugins-good.html)+# Install GStreamer Bad Plug-ins >= 1.8 (https://gstreamer.freedesktop.org/modules/gst-plugins-bad.html)+# Install Cabal, GHC == 8.0.2 (https://www.haskell.org/platform/prior.html)+# Install Cabal Install (https://www.haskell.org/platform/prior.html)+cabal install movie-monad+movie-monad+```++### [GitHub](https://github.com/lettier/movie-monad)++```bash+# Install GObject Introspection (https://wiki.gnome.org/action/show/Projects/GObjectIntrospection)+# Install GTK+ >= 3.10 (https://www.gtk.org/download/index.php)+# Install GStreamer >= 1.0 (https://gstreamer.freedesktop.org/download/)+# Install GStreamer Good Plug-ins (https://gstreamer.freedesktop.org/modules/gst-plugins-good.html)+# Install GStreamer Bad Plug-ins >= 1.8 (https://gstreamer.freedesktop.org/modules/gst-plugins-bad.html)+# Install Git (https://git-scm.com/downloads)+# Install GNU Make (https://www.gnu.org/software/make/)+# Install Haskell, GHC == 8.0.2 (https://www.haskell.org/platform/prior.html)+# Install Haksell Stack (https://www.haskell.org/platform/)+git clone https://github.com/lettier/movie-monad.git+cd movie-monad/+make+make install+make run+```++## Developer Documentation++[Let's make a GTK Video Player with Haskell](https://lettier.github.io/posts/2017-08-30-haskell-gtk-video-player.html) ## License
movie-monad.cabal view
@@ -1,5 +1,5 @@ name: movie-monad-version: 0.0.1.0+version: 0.0.2.0 synopsis: Plays videos using GStreamer and GTK+. description: Desktop video player that uses GStreamer and GTK+. homepage: https://github.com/lettier/movie-monad@@ -8,38 +8,84 @@ author: Lettier maintainer: Lettier copyright: 2017 David Lettier-category: Multimedia+category: Application+ , Multimedia , Multimedia Player , Video build-type: Simple-cabal-version: >=1.10+cabal-version: >= 1.10 extra-source-files: README.md , LICENSE+ , CHANGELOG.md , ./src/dev/Paths_movie_monad.hs , ./src/data/gui.glade-data-files: data/gui.glade+ , ./src/data/movie-monad-logo.svg+ , ./src/data/movie-monad-icon.png+ , ./src/Records.hs+ , ./src/Constants.hs+ , ./src/Window.hs+ , ./src/CommandLine.hs+ , ./src/Keyboard.hs+ , ./src/Mouse.hs+ , ./src/FileChooser.hs+ , ./src/Seek.hs+ , ./src/PlayPause.hs+ , ./src/VideoSizeSelector.hs+ , ./src/Fullscreen.hs+ , ./src/ErrorMessage.hs+ , ./src/About.hs+ , ./src/VideoInfo.hs+ , ./src/Playbin.hs+ , ./src/Uri.hs+ , ./src/Utils.hs+ , ./src/ScreensaverAndPowerManagement.hs+data-files: data/gui.glade+ , data/movie-monad-logo.svg+ , data/movie-monad-icon.png data-dir: ./src/ source-repository head type: git- location: https://github.com/lettier/gifcurry.git+ location: https://github.com/lettier/movie-monad.git executable movie-monad main-is: Main.hs default-language: Haskell2010 build-depends: base >= 4.7 && < 5+ , filepath == 1.4.*+ , system-fileio == 0.3.*+ , system-filepath == 0.4.* , text == 1.2.*+ , time == 1.6.* , process == 1.4.* , MissingH == 1.4.*- , haskell-gi-base == 0.20.*- , gi-xlib == 2.0.*+ , network-uri == 2.6.*+ , haskell-gi-base == 0.20.4 , gi-gobject == 2.0.* , gi-glib == 2.0.*- , gi-gdkx11 == 3.0.*- , gi-gstvideo == 1.0.* , gi-gdk == 3.0.*+ , gi-gdkpixbuf == 2.0.* , gi-gst == 1.0.*- , gi-gtk == 3.0.*- ghc-options: -threaded -with-rtsopts=-N -Wall- other-modules: Paths_movie_monad+ , gi-gstvideo == 1.0.*+ , gi-gtk == 3.0.18+ ghc-options: -threaded -with-rtsopts=-N -Wall -freverse-errors+ other-modules: Paths_movie_monad+ , Records+ , Constants+ , Window+ , CommandLine+ , Keyboard+ , Mouse+ , FileChooser+ , Seek+ , PlayPause+ , VideoSizeSelector+ , Fullscreen+ , ErrorMessage+ , About+ , VideoInfo+ , Uri+ , Playbin+ , Utils+ , ScreensaverAndPowerManagement hs-source-dirs: ./src/
+ src/About.hs view
@@ -0,0 +1,39 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++module About where++import Control.Monad+import qualified GI.Gdk+import qualified GI.Gtk++import qualified Records as R++addAboutHandler :: R.Application -> IO ()+addAboutHandler+ R.Application {+ R.guiObjects = R.GuiObjects {+ R.aboutButton = aboutButton+ , R.aboutDialog = aboutDialog+ }+ }+ =+ void (+ GI.Gtk.onWidgetButtonReleaseEvent aboutButton (aboutButtonReleaseHandler aboutDialog)+ )++aboutButtonReleaseHandler ::+ GI.Gtk.AboutDialog ->+ GI.Gdk.EventButton ->+ IO Bool+aboutButtonReleaseHandler aboutDialog _ =+ void (+ GI.Gtk.onDialogResponse aboutDialog (\ _ -> GI.Gtk.widgetHide aboutDialog)+ ) >>+ void (+ GI.Gtk.dialogRun aboutDialog+ ) >>+ return True
+ src/CommandLine.hs view
@@ -0,0 +1,32 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++module CommandLine where++import System.Environment+import Data.Text+import GI.Gtk++import qualified Records as R+import FileChooser+import Uri+import Utils++playVideoFromCommandLineIfNeeded :: R.Application -> IO ()+playVideoFromCommandLineIfNeeded+ application@R.Application {+ R.guiObjects = R.GuiObjects {+ R.fileChooserEntry = fileChooserEntry+ }+ }+ = do+ args <- getArgs+ case args of+ [] -> return ()+ (filePath':_) -> do+ filePath <- toAbsoluteUri filePath'+ GI.Gtk.entrySetText fileChooserEntry (Data.Text.pack filePath)+ fileChooserDialogResponseHandler application (enumToInt32 GI.Gtk.ResponseTypeOk)
+ src/Constants.hs view
@@ -0,0 +1,17 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}++module Constants where++import Data.Text++hideOnScreenControlsInterval :: Integral a => a+hideOnScreenControlsInterval = 5++invalidVideoWidgetName :: Data.Text.Text+invalidVideoWidgetName = "invalid-video-widget"
+ src/ErrorMessage.hs view
@@ -0,0 +1,42 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}++module ErrorMessage where++import Control.Monad+import Data.Text+import Data.Int+import qualified GI.Gtk++import qualified Records as R++addErrorMessageDialogHandler :: R.Application -> IO ()+addErrorMessageDialogHandler+ R.Application {+ R.guiObjects = R.GuiObjects {+ R.errorMessageDialog = errorMessageDialog+ }+ }+ =+ void (GI.Gtk.onDialogResponse errorMessageDialog (errorMessageDialogResponseHandler errorMessageDialog))++errorMessageDialogResponseHandler ::+ GI.Gtk.MessageDialog ->+ Int32 ->+ IO ()+errorMessageDialogResponseHandler errorMessageDialog _ =+ GI.Gtk.widgetHide errorMessageDialog >>+ GI.Gtk.setMessageDialogText errorMessageDialog "There was an error."++runErrorMessageDialog ::+ GI.Gtk.MessageDialog ->+ Text ->+ IO ()+runErrorMessageDialog errorMessageDialog text =+ GI.Gtk.setMessageDialogText errorMessageDialog text >>+ void (GI.Gtk.dialogRun errorMessageDialog)
+ src/FileChooser.hs view
@@ -0,0 +1,190 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}++module FileChooser where++import Control.Monad+import Data.Int+import Data.Text+import Data.IORef+import qualified Network.URI+import qualified GI.Gdk+import qualified GI.Gtk+import qualified GI.Gst++import qualified Records as R+import Constants+import Window+import PlayPause+import ErrorMessage+import VideoInfo+import Playbin+import Uri+import Utils++addFileChooserHandlers :: R.Application -> IO ()+addFileChooserHandlers+ application@R.Application {+ R.guiObjects = R.GuiObjects {+ R.fileChooserButton = fileChooserButton+ , R.fileChooserDialog = fileChooserDialog+ , R.fileChooserWidget = fileChooserWidget+ , R.fileChooserEntry = fileChooserEntry+ }+ }+ =+ void (GI.Gtk.onWidgetButtonReleaseEvent fileChooserButton (fileChooserButtonClickHandler application)) >>+ void (GI.Gtk.onDialogResponse fileChooserDialog (fileChooserDialogResponseHandler application)) >>+ void (GI.Gtk.onFileChooserSelectionChanged fileChooserWidget (fileChooserSelectionChangedHandler application)) >>+ void (GI.Gtk.onEntryIconRelease fileChooserEntry (\ _ _ -> GI.Gtk.entrySetText fileChooserEntry ""))++fileChooserDialogResponseHandler ::+ R.Application ->+ Int32 ->+ IO ()+fileChooserDialogResponseHandler+ R.Application {+ R.guiObjects = guiObjects@R.GuiObjects {+ R.window = window+ , R.fileChooserButton = fileChooserButton+ , R.videoWidget = videoWidget+ , R.seekScale = seekScale+ , R.playPauseButton = playPauseButton+ , R.videoWidthSelectionComboBox = videoWidthSelectionComboBox+ , R.fullscreenButton = fullscreenButton+ , R.fileChooserEntry = fileChooserEntry+ , R.fileChooserButtonLabel = fileChooserButtonLabel+ , R.volumeButton = volumeButton+ , R.errorMessageDialog = errorMessageDialog+ , R.fileChooserDialog = fileChooserDialog+ , R.playImage = playImage+ , R.pauseImage = pauseImage+ }+ , R.ioRefs = R.IORefs {+ R.isWindowFullScreenRef = isWindowFullScreenRef+ , R.videoInfoRef = videoInfoRef+ , R.previousFileNamePathRef = previousFileNamePathRef+ }+ , R.playbin = playbin+ }+ responseId+ = do+ GI.Gtk.widgetHide fileChooserDialog+ handleResponseType GI.Gtk.ResponseTypeOk+ where+ handleResponseType :: (Enum a, Ord a) => a -> IO ()+ handleResponseType enum+ | enumToInt32 enum == responseId = do+ _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull+ filePathName <- GI.Gtk.entryGetText fileChooserEntry+ let filePathNameStr = Data.Text.unpack filePathName+ (_, fileNameEmpty) <- setFileChooserButtonLabel fileChooserButtonLabel filePathName+ isWindowFullScreen <- readIORef isWindowFullScreenRef+ videoWidthSelection <- getSelectedVideoWidth videoWidthSelectionComboBox+ setPlaybinUriAndVolume playbin filePathNameStr volumeButton+ handleFileName+ fileNameEmpty+ filePathNameStr+ isWindowFullScreen+ videoWidthSelection+ | otherwise = do+ filePathName <- readIORef previousFileNamePathRef+ _ <- setFileChooserButtonLabel fileChooserButtonLabel filePathName+ GI.Gtk.entrySetText fileChooserEntry filePathName+ handleFileName ::+ Bool ->+ Prelude.String ->+ Bool ->+ Int ->+ IO ()+ handleFileName True _ _ _ = atomicWriteIORef videoInfoRef R.defaultVideoInfo >> resetWindow guiObjects+ handleFileName+ _+ filePathNameStr+ isWindowFullScreen+ videoWidthSelection+ = do+ retrievedVideoInfo <- getVideoInfo videoInfoRef filePathNameStr+ maybeWindowSize <- calculateWindowSize videoWidthSelection retrievedVideoInfo+ GI.Gtk.widgetSetSensitive seekScale True+ if R.isSeekable retrievedVideoInfo+ then GI.Gtk.widgetShow seekScale+ else GI.Gtk.widgetHide seekScale+ case maybeWindowSize of+ Nothing -> do+ resetWindow guiObjects+ runErrorMessageDialog+ errorMessageDialog+ (Data.Text.pack $ Prelude.concat ["\"", filePathNameStr, "\" is not a video."])+ Just (width, height) -> do+ videoWidgetName <- GI.Gtk.widgetGetName videoWidget+ if videoWidgetName == invalidVideoWidgetName+ then do+ resetWindow guiObjects+ runErrorMessageDialog+ errorMessageDialog+ "Cannot play the video. Please install the bad plugins, version 1.8 or higher, for GStreamer version 1."+ else do+ GI.Gtk.widgetShow videoWidget+ GI.Gtk.widgetShow playPauseButton+ GI.Gtk.widgetShow fullscreenButton+ setPlayPauseButton playPauseButton playImage pauseImage True+ unless isWindowFullScreen $+ setWindowSize width height fileChooserButton videoWidget window+ void $ GI.Gst.elementSetState playbin GI.Gst.StatePlaying++fileChooserButtonClickHandler ::+ R.Application ->+ GI.Gdk.EventButton ->+ IO Bool+fileChooserButtonClickHandler+ R.Application {+ R.guiObjects = R.GuiObjects {+ R.fileChooserEntry = fileChooserEntry+ , R.fileChooserDialog = fileChooserDialog+ }+ , R.ioRefs = R.IORefs {+ R.previousFileNamePathRef = previousFileNamePathRef+ }+ }+ _+ = do+ text <- GI.Gtk.entryGetText fileChooserEntry+ atomicWriteIORef previousFileNamePathRef text+ _ <- GI.Gtk.dialogRun fileChooserDialog+ return True++fileChooserSelectionChangedHandler ::+ R.Application ->+ IO ()+fileChooserSelectionChangedHandler+ R.Application {+ R.guiObjects = R.GuiObjects {+ R.fileChooserWidget = fileChooserWidget+ , R.fileChooserEntry = fileChooserEntry+ }+ , R.ioRefs = R.IORefs {+ R.videoInfoRef = videoInfoRef+ }+ }+ = do+ maybeUri <- GI.Gtk.fileChooserGetUri fileChooserWidget+ case maybeUri of+ Nothing -> return ()+ Just uri' -> do+ let uri = Network.URI.unEscapeString $ Data.Text.unpack uri'+ local <- isLocalFile uri+ video <- R.isVideo <$> getVideoInfo videoInfoRef uri+ GI.Gtk.entrySetText fileChooserEntry (if local && video then Data.Text.pack uri else "")++setFileChooserButtonLabel :: GI.Gtk.Label -> Data.Text.Text -> IO (Data.Text.Text, Bool)+setFileChooserButtonLabel fileChooserButtonLabel filePathName = do+ let fileName = fileNameFromFilePathName filePathName+ let fileNameEmpty = isTextEmpty fileName+ GI.Gtk.labelSetText fileChooserButtonLabel (if fileNameEmpty then "Open" else fileName)+ return (fileName, fileNameEmpty)
+ src/Fullscreen.hs view
@@ -0,0 +1,53 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++module Fullscreen where++import Control.Monad+import Data.IORef+import qualified GI.Gdk+import qualified GI.Gtk++import qualified Records as R++addFullscreenButtonReleaseHandler :: R.Application -> IO ()+addFullscreenButtonReleaseHandler+ application@R.Application {+ R.guiObjects = R.GuiObjects {+ R.fullscreenButton = fullscreenButton+ }+ }+ =+ void $ GI.Gtk.onWidgetButtonReleaseEvent fullscreenButton (fullscreenButtonReleaseHandler application)++fullscreenButtonReleaseHandler ::+ R.Application ->+ GI.Gdk.EventButton ->+ IO Bool+fullscreenButtonReleaseHandler+ R.Application {+ R.guiObjects = R.GuiObjects {+ R.window = window+ , R.fileChooserButton = fileChooserButton+ , R.videoWidthSelectionComboBox = videoWidthSelectionComboBox+ }+ , R.ioRefs = R.IORefs {+ R.isWindowFullScreenRef = isWindowFullScreenRef+ }+ }+ _+ = do+ isWindowFullScreen <- readIORef isWindowFullScreenRef+ if isWindowFullScreen+ then do+ GI.Gtk.widgetShow videoWidthSelectionComboBox+ GI.Gtk.widgetShow fileChooserButton+ void $ GI.Gtk.windowUnfullscreen window+ else do+ GI.Gtk.widgetHide videoWidthSelectionComboBox+ GI.Gtk.widgetHide fileChooserButton+ void $ GI.Gtk.windowFullscreen window+ return True
+ src/Keyboard.hs view
@@ -0,0 +1,74 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++module Keyboard where++import Control.Monad+import Data.IORef+import qualified GI.Gdk+import qualified GI.Gtk++import qualified Records as R+import Mouse+import PlayPause+import Fullscreen++addKeyboardEventHandler :: R.Application -> IO ()+addKeyboardEventHandler+ application@R.Application {+ R.guiObjects = R.GuiObjects {+ R.window = window+ }+ }+ =+ void $ GI.Gtk.onWidgetKeyPressEvent window $ keyboardEventHandler application++keyboardEventHandler ::+ R.Application ->+ GI.Gdk.EventKey ->+ IO Bool+keyboardEventHandler+ application@R.Application {+ R.guiObjects = R.GuiObjects {+ R.volumeButton = volumeButton+ }+ , R.ioRefs = R.IORefs {+ R.videoInfoRef = videoInfoRef+ }+ }+ eventKey+ = do+ videoInfoGathered <- readIORef videoInfoRef+ let isVideo = R.isVideo videoInfoGathered+ let volumeDelta = 0.05+ oldVolume <- GI.Gtk.scaleButtonGetValue volumeButton+ keyValue <- GI.Gdk.getEventKeyKeyval eventKey+ eventButton <- GI.Gdk.newZeroEventButton+ -- Mute Toggle+ when (keyValue == GI.Gdk.KEY_m || keyValue == GI.Gdk.KEY_AudioMute) $ do+ let newVolume = if oldVolume <= 0.0 then 0.5 else 0.0+ GI.Gtk.scaleButtonSetValue volumeButton newVolume+ -- Play/Pause Toggle+ when ((keyValue == GI.Gdk.KEY_space || keyValue == GI.Gdk.KEY_AudioPlay) && isVideo) $+ void $ playPauseButtonClickHandler application eventButton+ -- Volume Up+ when (keyValue == GI.Gdk.KEY_Up || keyValue == GI.Gdk.KEY_AudioRaiseVolume) $ do+ let newVolume = if oldVolume >= 1.0 then 1.0 else oldVolume + volumeDelta+ GI.Gtk.scaleButtonSetValue volumeButton newVolume+ -- Volume Down+ when (keyValue == GI.Gdk.KEY_Down || keyValue == GI.Gdk.KEY_AudioLowerVolume) $ do+ let newVolume = if oldVolume <= 0.0 then 0.0 else oldVolume - volumeDelta+ GI.Gtk.scaleButtonSetValue volumeButton newVolume+ -- Show Controls+ when (keyValue == GI.Gdk.KEY_c) $ do+ eventMotion <- GI.Gdk.newZeroEventMotion+ void $ windowMouseMoveHandler application eventMotion+ -- Fullscreen Toggle+ when (keyValue == GI.Gdk.KEY_f && isVideo) $ do+ eventMotion <- GI.Gdk.newZeroEventMotion+ void $ windowMouseMoveHandler application eventMotion+ void $ fullscreenButtonReleaseHandler application eventButton+ return True
src/Main.hs view
@@ -10,26 +10,35 @@ module Main where import Prelude-import Foreign.C.Types-import System.Process-import System.Exit-import Control.Monad-import Control.Exception-import Text.Read+import System.Info import Data.IORef import Data.Maybe-import Data.Int import Data.Text import Data.GI.Base-import Data.GI.Base.Signals import Data.GI.Base.Properties-import GI.GLib import GI.GObject import qualified GI.Gtk import GI.Gst import GI.GstVideo-import GI.Gdk-import GI.GdkX11+import GI.GdkPixbuf++import qualified Records as R+import Constants+import Window+import CommandLine+import Mouse+import Keyboard+import FileChooser+import Seek+import PlayPause+import Fullscreen+import ErrorMessage+import About+import VideoSizeSelector+import Playbin+import ScreensaverAndPowerManagement (disable, enable)+import Utils+ import Paths_movie_monad -- Declare Element a type instance of IsVideoOverlay via a newtype wrapper@@ -47,59 +56,119 @@ 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"+ fileChooserButton <- builderGetObject GI.Gtk.Button builder "file-chooser-button"+ fileChooserButtonLabel <- builderGetObject GI.Gtk.Label builder "file-chooser-button-label"+ fileChooserDialog <- builderGetObject GI.Gtk.Dialog builder "file-chooser-dialog"+ fileChooserEntry <- builderGetObject GI.Gtk.Entry builder "file-chooser-entry"+ fileChooserWidget <- builderGetObject GI.Gtk.FileChooserWidget builder "file-chooser-widget"+ fileChooserCancelButton <- builderGetObject GI.Gtk.Button builder "file-chooser-cancel-button"+ fileChooserOpenButton <- builderGetObject GI.Gtk.Button builder "file-chooser-open-button"+ videoWidgetBox <- builderGetObject GI.Gtk.Box builder "video-widget-box"+ bottomControlsGtkBox <- builderGetObject GI.Gtk.Box builder "bottom-controls-gtk-box" seekScale <- builderGetObject GI.Gtk.Scale builder "seek-scale"- onOffSwitch <- builderGetObject GI.Gtk.Switch builder "on-off-switch"+ playPauseButton <- builderGetObject GI.Gtk.Button builder "play-pause-button"+ playImage <- builderGetObject GI.Gtk.Image builder "play-image"+ pauseImage <- builderGetObject GI.Gtk.Image builder "pause-image" volumeButton <- builderGetObject GI.Gtk.VolumeButton builder "volume-button"- desiredVideoWidthComboBox <- builderGetObject GI.Gtk.ComboBoxText builder "desired-video-width-combo-box"+ videoWidthSelectionComboBox <- builderGetObject GI.Gtk.ComboBoxText builder "video-width-selection-combo-box" fullscreenButton <- builderGetObject GI.Gtk.Button builder "fullscreen-button"+ bufferingSpinner <- builderGetObject GI.Gtk.Spinner builder "buffering-spinner" 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")-- isWindowFullScreenRef <- newIORef False-- _ <- GI.Gtk.onWidgetRealize drawingArea $ onDrawingAreaRealize drawingArea playbin fullscreenButton+ logoFile <- getDataFileName "data/movie-monad-logo.svg"+ logo <- GI.GdkPixbuf.pixbufNewFromFile (pack logoFile)+ GI.Gtk.aboutDialogSetLogo aboutDialog (Just logo) - _ <- GI.Gtk.onFileChooserButtonFileSet fileChooserButton $- onFileChooserButtonFileSet- playbin- fileChooserButton- volumeButton- isWindowFullScreenRef- desiredVideoWidthComboBox- onOffSwitch- fullscreenButton- drawingArea- window- errorMessageDialog+ -- Glade does not allow us to use the response ID nicknames so we programmatically set them here.+ GI.Gtk.dialogAddActionWidget fileChooserDialog fileChooserCancelButton (enumToInt32 GI.Gtk.ResponseTypeCancel)+ GI.Gtk.dialogAddActionWidget fileChooserDialog fileChooserOpenButton (enumToInt32 GI.Gtk.ResponseTypeOk) - _ <- GI.Gtk.onSwitchStateSet onOffSwitch (onSwitchStateSet playbin)+ isWindowFullScreenRef <- newIORef False+ mouseMovedLastRef <- newIORef 0+ previousFileNamePathRef <- newIORef ""+ videoInfoRef <- newIORef R.defaultVideoInfo - _ <- GI.Gtk.onScaleButtonValueChanged volumeButton (onScaleButtonValueChanged playbin)+ let ioRefs = R.IORefs {+ R.isWindowFullScreenRef = isWindowFullScreenRef+ , R.mouseMovedLastRef = mouseMovedLastRef+ , R.previousFileNamePathRef = previousFileNamePathRef+ , R.videoInfoRef = videoInfoRef+ } - seekScaleHandlerId <- GI.Gtk.onRangeValueChanged seekScale (onRangeValueChanged playbin seekScale)+ playbin <- fromJust <$> GI.Gst.elementFactoryMake "playbin" (Just "MultimediaPlayerPlaybin")+ maybeGtkSink <- GI.Gst.elementFactoryMake "gtksink" (Just "MultimediaPlayerGtkSink")+ videoWidget <-+ case maybeGtkSink of+ Nothing -> do+ putStrLn "Could not create a GtkSink. Please install the bad plugins, version 1.8 or higher, for GStreamer 1."+ drawingArea <- GI.Gtk.drawingAreaNew+ GI.Gtk.widgetSetName drawingArea invalidVideoWidgetName+ GI.Gtk.unsafeCastTo GI.Gtk.Widget drawingArea+ Just gtkSink ->+ fromJust <$> Data.GI.Base.Properties.getObjectPropertyObject gtkSink "widget" GI.Gtk.Widget - _ <- GI.GLib.timeoutAddSeconds GI.GLib.PRIORITY_DEFAULT 1 (updateSeekScale playbin seekScale seekScaleHandlerId)+ Data.GI.Base.Properties.setObjectPropertyObject playbin "video-sink" maybeGtkSink+ Data.GI.Base.Properties.setObjectPropertyBool playbin "force-aspect-ratio" True+ GI.Gtk.boxPackStart videoWidgetBox videoWidget True True 0+ GI.Gtk.widgetSetHexpand videoWidget True+ GI.Gtk.widgetSetVexpand videoWidget True+ GI.Gtk.widgetSetSensitive videoWidget True - _ <- GI.Gtk.onComboBoxChanged desiredVideoWidthComboBox $- onComboBoxChanged fileChooserButton desiredVideoWidthComboBox drawingArea window+ playbinBus <- GI.Gst.elementGetBus playbin - _ <- GI.Gtk.onWidgetButtonReleaseEvent fullscreenButton- (onFullscreenButtonRelease isWindowFullScreenRef desiredVideoWidthComboBox fileChooserButton window)+ let guiObjects = R.GuiObjects {+ R.window = window+ , R.fileChooserButton = fileChooserButton+ , R.fileChooserButtonLabel = fileChooserButtonLabel+ , R.fileChooserDialog = fileChooserDialog+ , R.fileChooserEntry = fileChooserEntry+ , R.fileChooserWidget = fileChooserWidget+ , R.fileChooserCancelButton = fileChooserCancelButton+ , R.fileChooserOpenButton = fileChooserOpenButton+ , R.videoWidget = videoWidget+ , R.bottomControlsGtkBox = bottomControlsGtkBox+ , R.seekScale = seekScale+ , R.playPauseButton = playPauseButton+ , R.playImage = playImage+ , R.pauseImage = pauseImage+ , R.volumeButton = volumeButton+ , R.videoWidthSelectionComboBox = videoWidthSelectionComboBox+ , R.fullscreenButton = fullscreenButton+ , R.bufferingSpinner = bufferingSpinner+ , R.errorMessageDialog = errorMessageDialog+ , R.aboutButton = aboutButton+ , R.aboutDialog = aboutDialog+ } - _ <- GI.Gtk.onWidgetWindowStateEvent window (onWidgetWindowStateEvent isWindowFullScreenRef)+ let application = R.Application {+ R.guiObjects = guiObjects+ , R.ioRefs = ioRefs+ , R.playbin = playbin+ , R.playbinBus = playbinBus+ } - _ <- GI.Gtk.onWidgetButtonReleaseEvent aboutButton (onAboutButtonRelease aboutDialog)+ addWindowHandlers application [playVideoFromCommandLineIfNeeded]+ addPlaybinHandler application+ addFileChooserHandlers application+ addPlayPauseButtonClickHandler application+ addSeekHandlers application+ addVideoSizeSelectorHandler application+ addFullscreenButtonReleaseHandler application+ addWindowMouseMoveHandlers application+ addAboutHandler application+ addKeyboardEventHandler application+ addErrorMessageDialogHandler application - _ <- GI.Gtk.onWidgetDestroy window (onWindowDestroy playbin)+ let operatingSystem = System.Info.os+ screenAndPowerManagementActions <- ScreensaverAndPowerManagement.disable operatingSystem GI.Gtk.widgetShowAll window GI.Gtk.main + ScreensaverAndPowerManagement.enable operatingSystem screenAndPowerManagementActions+ builderGetObject :: (GI.GObject.GObject b, GI.Gtk.IsBuilder a) => (Data.GI.Base.ManagedPtr b -> b) ->@@ -109,284 +178,3 @@ builderGetObject objectTypeClass builder objectId = fromJust <$> GI.Gtk.builderGetObject builder (pack objectId) >>= GI.Gtk.unsafeCastTo objectTypeClass--onDrawingAreaRealize ::- GI.Gtk.Widget ->- GI.Gst.Element ->- GI.Gtk.Button ->- GI.Gtk.WidgetRealizeCallback-onDrawingAreaRealize drawingArea playbin fullscreenButton = 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.widgetHide fullscreenButton--onFileChooserButtonFileSet ::- GI.Gst.Element ->- GI.Gtk.FileChooserButton ->- GI.Gtk.VolumeButton ->- IORef Bool ->- GI.Gtk.ComboBoxText ->- GI.Gtk.Switch ->- GI.Gtk.Button ->- GI.Gtk.Widget ->- GI.Gtk.Window ->- GI.Gtk.MessageDialog ->- GI.Gtk.FileChooserButtonFileSetCallback-onFileChooserButtonFileSet- playbin- fileChooserButton- volumeButton- isWindowFullScreenRef- desiredVideoWidthComboBox- onOffSwitch- fullscreenButton- drawingArea- window- errorMessageDialog- = do- _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull-- filename <- fromJust <$> GI.Gtk.fileChooserGetFilename fileChooserButton-- setPlaybinUriAndVolume playbin filename volumeButton-- isWindowFullScreen <- readIORef isWindowFullScreenRef-- desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox- maybeWindowSize <- getWindowSize desiredVideoWidth filename-- case maybeWindowSize of- Nothing -> do- _ <- GI.Gst.elementSetState playbin GI.Gst.StatePaused- GI.Gtk.windowUnfullscreen window- GI.Gtk.switchSetActive onOffSwitch False- GI.Gtk.widgetHide fullscreenButton- GI.Gtk.widgetShow desiredVideoWidthComboBox- resetWindowSize desiredVideoWidth fileChooserButton drawingArea window- _ <- GI.Gtk.onDialogResponse errorMessageDialog (\ _ -> GI.Gtk.widgetHide errorMessageDialog)- void $ GI.Gtk.dialogRun errorMessageDialog- Just (width, height) -> do- _ <- GI.Gst.elementSetState playbin GI.Gst.StatePlaying- GI.Gtk.switchSetActive onOffSwitch True- GI.Gtk.widgetShow fullscreenButton- unless isWindowFullScreen $ setWindowSize width height fileChooserButton drawingArea window--onSwitchStateSet ::- GI.Gst.Element ->- Bool ->- IO Bool-onSwitchStateSet playbin switchOn = do- if switchOn- then void $ GI.Gst.elementSetState playbin GI.Gst.StatePlaying- else void $ GI.Gst.elementSetState playbin GI.Gst.StatePaused- return switchOn--onScaleButtonValueChanged ::- GI.Gst.Element ->- Double ->- IO ()-onScaleButtonValueChanged playbin volume =- void $ Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume--onRangeValueChanged ::- GI.Gst.Element ->- GI.Gtk.Scale ->- IO ()-onRangeValueChanged playbin 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--updateSeekScale ::- GI.Gst.Element ->- GI.Gtk.Scale ->- Data.GI.Base.Signals.SignalHandlerId ->- IO Bool-updateSeekScale playbin seekScale seekScaleHandlerId = 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--onComboBoxChanged ::- GI.Gtk.FileChooserButton ->- GI.Gtk.ComboBoxText ->- GI.Gtk.Widget ->- GI.Gtk.Window ->- IO ()-onComboBoxChanged- fileChooserButton- desiredVideoWidthComboBox- drawingArea- window- = do- filename' <- GI.Gtk.fileChooserGetFilename fileChooserButton- let filename = fromMaybe "" filename'-- desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox- maybeWindowSize <- getWindowSize desiredVideoWidth filename-- case maybeWindowSize of- Nothing -> resetWindowSize desiredVideoWidth fileChooserButton drawingArea window- Just (width, height) -> setWindowSize width height fileChooserButton drawingArea window--onFullscreenButtonRelease ::- IORef Bool ->- GI.Gtk.ComboBoxText ->- GI.Gtk.FileChooserButton ->- GI.Gtk.Window ->- GI.Gdk.EventButton ->- IO Bool-onFullscreenButtonRelease- isWindowFullScreenRef- desiredVideoWidthComboBox- fileChooserButton- window- _- = do- isWindowFullScreen <- readIORef isWindowFullScreenRef- if isWindowFullScreen- then do- GI.Gtk.widgetShow desiredVideoWidthComboBox- GI.Gtk.widgetShow fileChooserButton- void $ GI.Gtk.windowUnfullscreen window- else do- GI.Gtk.widgetHide desiredVideoWidthComboBox- GI.Gtk.widgetHide fileChooserButton- void $ GI.Gtk.windowFullscreen window- return True--onWidgetWindowStateEvent ::- IORef Bool ->- GI.Gdk.EventWindowState ->- IO Bool-onWidgetWindowStateEvent isWindowFullScreenRef eventWindowState = do- windowStates <- GI.Gdk.getEventWindowStateNewWindowState eventWindowState- let isWindowFullScreen = Prelude.foldl (\ acc x -> acc || GI.Gdk.WindowStateFullscreen == x) False windowStates- writeIORef isWindowFullScreenRef isWindowFullScreen- return True--onAboutButtonRelease ::- GI.Gtk.AboutDialog ->- GI.Gdk.EventButton ->- IO Bool-onAboutButtonRelease aboutDialog _ = do- _ <- GI.Gtk.onDialogResponse aboutDialog (\ _ -> GI.Gtk.widgetHide aboutDialog)- _ <- GI.Gtk.dialogRun aboutDialog- return True--onWindowDestroy ::- GI.Gst.Element ->- IO ()-onWindowDestroy playbin = do- _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull- _ <- GI.Gst.objectUnref playbin- GI.Gtk.mainQuit--setPlaybinUriAndVolume ::- GI.Gst.Element ->- Prelude.String ->- GI.Gtk.VolumeButton ->- IO ()-setPlaybinUriAndVolume playbin filename volumeButton = do- 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)--getVideoInfo :: Prelude.String -> Prelude.String -> IO (Maybe Prelude.String)-getVideoInfo flag filename = do- (code, out, _) <- catch (- readProcessWithExitCode- "exiftool"- [flag, "-s", "-S", filename]- ""- ) (\ (_ :: Control.Exception.IOException) -> return (ExitFailure 1, "", ""))- if code == System.Exit.ExitSuccess- then return (Just out)- else return Nothing--isVideo :: Prelude.String -> IO Bool-isVideo filename = do- maybeOut <- getVideoInfo "-MIMEType" filename- case maybeOut of- Nothing -> return False- Just out -> return ("video" `isInfixOf` pack out)--getWindowSize :: Int -> Prelude.String -> IO (Maybe (Int32, Int32))-getWindowSize desiredVideoWidth filename =- isVideo filename >>=- getWidthHeightString >>=- splitWidthHeightString >>=- widthHeightToDouble >>=- ratio >>=- windowSize- where- getWidthHeightString :: Bool -> IO (Maybe Prelude.String)- getWidthHeightString False = return Nothing- getWidthHeightString True = getVideoInfo "-ImageSize" filename- splitWidthHeightString :: Maybe Prelude.String -> IO (Maybe [Text])- splitWidthHeightString Nothing = return Nothing- splitWidthHeightString (Just string) = return (Just (Data.Text.splitOn "x" (pack string)))- widthHeightToDouble :: Maybe [Text] -> IO (Maybe Double, Maybe Double)- widthHeightToDouble (Just (x:y:_)) = return (readMaybe (unpack x) :: Maybe Double, readMaybe (unpack y) :: Maybe Double)- widthHeightToDouble _ = return (Nothing, Nothing)- ratio :: (Maybe Double, Maybe Double) -> IO (Maybe Double)- ratio (Just width, Just height) =- if width <= 0.0 then return Nothing else return (Just (height / width))- ratio _ = return Nothing- windowSize :: Maybe Double -> IO (Maybe (Int32, Int32))- windowSize Nothing = return Nothing- windowSize (Just ratio') =- return (Just (fromIntegral desiredVideoWidth :: Int32, round ((fromIntegral desiredVideoWidth :: Double) * ratio') :: Int32))--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- GI.Gtk.widgetQueueDraw drawingArea- setWindowSize width 0 fileChooserButton drawingArea window
+ src/Mouse.hs view
@@ -0,0 +1,69 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++module Mouse where++import Control.Monad+import Data.Maybe+import Data.Text+import Data.IORef+import Data.Time.Clock.POSIX+import qualified GI.Gdk+import qualified GI.Gtk++import qualified Records as R++addWindowMouseMoveHandlers :: R.Application -> IO ()+addWindowMouseMoveHandlers+ application@R.Application {+ R.guiObjects = R.GuiObjects {+ R.videoWidget = videoWidget+ , R.seekScale = seekScale+ }+ }+ =+ void (GI.Gtk.onWidgetMotionNotifyEvent videoWidget (windowMouseMoveHandler application)) >>+ void (GI.Gtk.onWidgetMotionNotifyEvent seekScale (windowMouseMoveHandler application))++windowMouseMoveHandler ::+ R.Application ->+ GI.Gdk.EventMotion ->+ IO Bool+windowMouseMoveHandler+ R.Application {+ R.guiObjects = R.GuiObjects {+ R.window = window+ , R.fileChooserButton = fileChooserButton+ , R.bottomControlsGtkBox = bottomControlsGtkBox+ }+ , R.ioRefs = R.IORefs {+ R.isWindowFullScreenRef = isWindowFullScreenRef+ , R.mouseMovedLastRef = mouseMovedLastRef+ }+ }+ _+ = do+ isWindowFullScreen <- readIORef isWindowFullScreenRef+ unless isWindowFullScreen $ GI.Gtk.widgetShow fileChooserButton+ GI.Gtk.widgetShow bottomControlsGtkBox+ setCursor window Nothing+ timeNow <- getPOSIXTime+ atomicWriteIORef mouseMovedLastRef (round timeNow)+ return False++setCursor :: GI.Gtk.Window -> Maybe Text -> IO ()+setCursor window Nothing =+ fromJust <$> GI.Gtk.widgetGetWindow window >>=+ flip GI.Gdk.windowSetCursor (Nothing :: Maybe GI.Gdk.Cursor)+setCursor window (Just cursorType) = do+ gdkWindow <- fromJust <$> GI.Gtk.widgetGetWindow window+ maybeCursor <- makeCursor gdkWindow cursorType+ GI.Gdk.windowSetCursor gdkWindow maybeCursor++makeCursor :: GI.Gdk.Window -> Text -> IO (Maybe GI.Gdk.Cursor)+makeCursor window cursorType =+ GI.Gdk.windowGetDisplay window >>=+ flip GI.Gdk.cursorNewFromName cursorType
+ src/PlayPause.hs view
@@ -0,0 +1,81 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}++module PlayPause where++import Control.Monad+import Data.Text+import qualified GI.Gdk+import qualified GI.Gtk+import qualified GI.Gst++import qualified Records as R++addPlayPauseButtonClickHandler :: R.Application -> IO ()+addPlayPauseButtonClickHandler+ application@R.Application {+ R.guiObjects = R.GuiObjects {+ R.playPauseButton = playPauseButton+ }+ }+ =+ void $ GI.Gtk.onWidgetButtonReleaseEvent playPauseButton (playPauseButtonClickHandler application)++playPauseButtonClickHandler ::+ R.Application ->+ GI.Gdk.EventButton ->+ IO Bool+playPauseButtonClickHandler+ R.Application {+ R.guiObjects = R.GuiObjects {+ R.playPauseButton = playPauseButton+ , R.playImage = playImage+ , R.pauseImage = pauseImage+ }+ , R.playbin = playbin+ }+ _+ = do+ isPlaying <- isPlayPauseButtonPlaying playPauseButton+ if isPlaying+ then do+ setPlayPauseButton playPauseButton playImage pauseImage False+ void $ GI.Gst.elementSetState playbin GI.Gst.StatePaused+ else do+ setPlayPauseButton playPauseButton playImage pauseImage True+ void $ GI.Gst.elementSetState playbin GI.Gst.StatePlaying+ return False++setPlayPauseButton ::+ GI.Gtk.Button ->+ GI.Gtk.Image ->+ GI.Gtk.Image ->+ Bool ->+ IO ()+setPlayPauseButton playPauseButton _ pauseImage True = do+ GI.Gtk.buttonSetImage playPauseButton (Just pauseImage)+ GI.Gtk.widgetSetTooltipText playPauseButton (Just "Click to pause")+setPlayPauseButton playPauseButton playImage _ False = do+ GI.Gtk.buttonSetImage playPauseButton (Just playImage)+ GI.Gtk.widgetSetTooltipText playPauseButton (Just "Click to play")++isPlayPauseButtonPlaying ::+ GI.Gtk.Button ->+ IO Bool+isPlayPauseButtonPlaying playPauseButton =+ GI.Gtk.buttonGetImage playPauseButton >>= getImage >>= getName >>= getMatch+ where+ getImage :: Maybe GI.Gtk.Widget -> IO (Maybe GI.Gtk.Image)+ getImage Nothing = return Nothing+ getImage (Just widget) = GI.Gtk.castTo GI.Gtk.Image widget+ getName :: Maybe GI.Gtk.Image -> IO (Maybe Text)+ getName Nothing = return Nothing+ getName (Just image) = GI.Gtk.getImageStock image+ getMatch :: Maybe Text -> IO Bool+ getMatch Nothing = return False+ getMatch (Just text) = return ("gtk-media-pause" == text)
+ src/Playbin.hs view
@@ -0,0 +1,122 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}++module Playbin where++import Control.Monad+import Data.Text+import Data.GI.Base.Properties+import GI.GLib+import qualified GI.Gtk+import qualified GI.Gst++import qualified Records as R+import Window+import PlayPause+import ErrorMessage+import Uri++addPlaybinHandler :: R.Application -> IO ()+addPlaybinHandler+ R.Application {+ R.guiObjects = guiObjects@R.GuiObjects {+ R.volumeButton = volumeButton+ }+ , R.playbin = playbin+ , R.playbinBus = playbinBus+ }+ =+ void(+ GI.Gst.busAddWatch playbinBus GI.GLib.PRIORITY_DEFAULT (+ pipelineBusMessageHandler+ guiObjects+ playbin+ )+ ) >>+ void (+ GI.Gtk.onScaleButtonValueChanged volumeButton (volumeButtonValueChangedHandler playbin)+ )++pipelineBusMessageHandler ::+ R.GuiObjects ->+ GI.Gst.Element ->+ GI.Gst.Bus ->+ GI.Gst.Message ->+ IO Bool+pipelineBusMessageHandler+ guiObjects@R.GuiObjects {+ R.seekScale = seekScale+ , R.fileChooserEntry = fileChooserEntry+ , R.fileChooserButtonLabel = fileChooserButtonLabel+ , R.volumeButton = volumeButton+ , R.errorMessageDialog = errorMessageDialog+ , R.bufferingSpinner = bufferingSpinner+ , R.playPauseButton = playPauseButton+ }+ playbin+ _+ message+ = do+ messageTypes <- GI.Gst.getMessageType message+ let messageType = case messageTypes of+ [] -> GI.Gst.MessageTypeUnknown+ (msg:_) -> msg+ entryText <- GI.Gtk.entryGetText fileChooserEntry+ labelText <- GI.Gtk.labelGetText fileChooserButtonLabel+ when (+ messageType == GI.Gst.MessageTypeError &&+ (+ (not . Data.Text.null) entryText ||+ labelText /= "Open"+ )+ ) $ do+ (gError, text) <- GI.Gst.messageParseError message+ gErrorText <- GI.Gst.gerrorMessage gError+ Prelude.mapM_ print [text, "\n", gErrorText]+ GI.Gtk.entrySetText fileChooserEntry ""+ GI.Gtk.labelSetText fileChooserButtonLabel "Open"+ _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull+ setPlaybinUriAndVolume playbin "" volumeButton+ resetWindow guiObjects+ runErrorMessageDialog+ errorMessageDialog+ (Data.Text.concat ["There was a problem trying to play the video \"", entryText, "\"."])+ when (messageType == GI.Gst.MessageTypeBuffering) $ do+ percent <- GI.Gst.messageParseBuffering message+ isPlaying <- isPlayPauseButtonPlaying playPauseButton+ if percent >= 100+ then do+ GI.Gtk.widgetHide bufferingSpinner+ GI.Gtk.setSpinnerActive bufferingSpinner False+ GI.Gtk.widgetSetSensitive seekScale True+ when isPlaying $ void $ GI.Gst.elementSetState playbin GI.Gst.StatePlaying+ else do+ GI.Gtk.widgetShow bufferingSpinner+ GI.Gtk.setSpinnerActive bufferingSpinner True+ GI.Gtk.widgetSetSensitive seekScale False+ void $ GI.Gst.elementSetState playbin GI.Gst.StatePaused+ return ()+ return True++volumeButtonValueChangedHandler ::+ GI.Gst.Element ->+ Double ->+ IO ()+volumeButtonValueChangedHandler playbin volume =+ void $ Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume++setPlaybinUriAndVolume ::+ GI.Gst.Element ->+ Prelude.String ->+ GI.Gtk.VolumeButton ->+ IO ()+setPlaybinUriAndVolume playbin fileName volumeButton = do+ uri <- addUriSchemeIfNone fileName+ volume <- GI.Gtk.scaleButtonGetValue volumeButton+ Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume+ Data.GI.Base.Properties.setObjectPropertyString playbin "uri" (Just $ pack uri)
+ src/Records.hs view
@@ -0,0 +1,76 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++module Records where++import Data.IORef+import Data.Text+import qualified GI.Gtk+import qualified GI.Gst++data VideoInfo = VideoInfo {+ uri :: Prelude.String+ , isLocalFile :: Bool+ , isVideo :: Bool+ , isSeekable :: Bool+ , videoWidth :: Int+ , videoHeight :: Int+ }++defaultVideoInfo :: VideoInfo+defaultVideoInfo = VideoInfo {+ uri = ""+ , isLocalFile = False+ , isVideo = False+ , isSeekable = False+ , videoWidth = 800+ , videoHeight = 600+ }++data GuiObjects = GuiObjects {+ window :: GI.Gtk.Window+ , fileChooserButton :: GI.Gtk.Button+ , fileChooserButtonLabel :: GI.Gtk.Label+ , fileChooserDialog :: GI.Gtk.Dialog+ , fileChooserEntry :: GI.Gtk.Entry+ , fileChooserWidget :: GI.Gtk.FileChooserWidget+ , fileChooserCancelButton :: GI.Gtk.Button+ , fileChooserOpenButton :: GI.Gtk.Button+ , videoWidget :: GI.Gtk.Widget+ , bottomControlsGtkBox :: GI.Gtk.Box+ , seekScale :: GI.Gtk.Scale+ , playPauseButton :: GI.Gtk.Button+ , playImage :: GI.Gtk.Image+ , pauseImage :: GI.Gtk.Image+ , volumeButton :: GI.Gtk.VolumeButton+ , videoWidthSelectionComboBox :: GI.Gtk.ComboBoxText+ , fullscreenButton :: GI.Gtk.Button+ , bufferingSpinner :: GI.Gtk.Spinner+ , errorMessageDialog :: GI.Gtk.MessageDialog+ , aboutButton :: GI.Gtk.Button+ , aboutDialog :: GI.Gtk.AboutDialog+ }++data IORefs = IORefs {+ isWindowFullScreenRef :: IORef Bool+ , mouseMovedLastRef :: IORef Integer+ , previousFileNamePathRef :: IORef Data.Text.Text+ , videoInfoRef :: IORef VideoInfo+ }++data ScreensaverAndPowerManagementActions = ScreensaverAndPowerManagementActions {+ disabledPowerManagement :: Bool+ , disabledXScreensaver :: Bool+ , disabledCinnamonScreensaver :: Bool+ , disabledGnomeScreensaver :: Bool+ }++data Application = Application {+ guiObjects :: GuiObjects+ , ioRefs :: IORefs+ , playbin :: GI.Gst.Element+ , playbinBus :: GI.Gst.Bus+ }
+ src/ScreensaverAndPowerManagement.hs view
@@ -0,0 +1,108 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module ScreensaverAndPowerManagement where++import System.Process+import Control.Monad+import Data.Text++import qualified Records as R (ScreensaverAndPowerManagementActions(..))+import Utils (safeRunProcessGetOutput, linuxProcessIsRunning, spawnLinuxProcessAndDisown)++disable :: String -> IO R.ScreensaverAndPowerManagementActions+disable "linux" = do+ disabledPowerManagement <- disablePowerManagement+ disabledXScreensaver <- disableXScreensaver+ disabledCinnamonScreensaver <- disableCinnamonScreensaver+ disabledGnomeScreensaver <- disableGnomeScreensaver+ return R.ScreensaverAndPowerManagementActions {+ R.disabledPowerManagement = disabledPowerManagement+ , R.disabledXScreensaver = disabledXScreensaver+ , R.disabledCinnamonScreensaver = disabledCinnamonScreensaver+ , R.disabledGnomeScreensaver = disabledGnomeScreensaver+ }+disable _ =+ return R.ScreensaverAndPowerManagementActions {+ R.disabledPowerManagement = False+ , R.disabledXScreensaver = False+ , R.disabledCinnamonScreensaver = False+ , R.disabledGnomeScreensaver = False+ }++enable :: String -> R.ScreensaverAndPowerManagementActions -> IO ()+enable+ "linux"+ R.ScreensaverAndPowerManagementActions {+ R.disabledPowerManagement = disabledPowerManagement+ , R.disabledXScreensaver = disabledXScreensaver+ , R.disabledCinnamonScreensaver = disabledCinnamonScreensaver+ , R.disabledGnomeScreensaver = disabledGnomeScreensaver+ }+ = do+ when disabledPowerManagement enablePowerManagement+ when disabledXScreensaver enableXScreensaver+ when disabledCinnamonScreensaver enableCinnamonScreensaver+ when disabledGnomeScreensaver enableGnomeScreensaver+ return ()+enable _ _ = return ()++disablePowerManagement :: IO Bool+disablePowerManagement = do+ (_, out, _) <- safeRunProcessGetOutput "xset" ["-q"]+ let isEnabled =+ Data.Text.isInfixOf "dpms is enabled" $+ Data.Text.toLower $+ Data.Text.pack out+ if isEnabled+ then do+ _ <- System.Process.spawnCommand "sleep 1; xset -dpms s off"+ return True+ else return False++enablePowerManagement :: IO ()+enablePowerManagement =+ void $+ System.Process.spawnCommand "sleep 1; xset +dpms s on"++disableXScreensaver :: IO Bool+disableXScreensaver =+ disableScreensaver+ "xscreensaver"+ "xscreensaver-command --exit"++enableXScreensaver :: IO ()+enableXScreensaver = spawnLinuxProcessAndDisown "xscreensaver -no-splash"++disableCinnamonScreensaver :: IO Bool+disableCinnamonScreensaver =+ disableScreensaver+ "cinnamon-screensaver"+ "cinnamon-screensaver-command --exit"++enableCinnamonScreensaver :: IO ()+enableCinnamonScreensaver = spawnLinuxProcessAndDisown "cinnamon-screensaver"++disableGnomeScreensaver :: IO Bool+disableGnomeScreensaver =+ disableScreensaver+ "gnome-screensaver"+ "gnome-screensaver-command --exit"++enableGnomeScreensaver :: IO ()+enableGnomeScreensaver = spawnLinuxProcessAndDisown "gnome-screensaver"++disableScreensaver :: String -> String -> IO Bool+disableScreensaver processName disableCommand = do+ isRunning <- linuxProcessIsRunning processName+ if isRunning+ then do+ spawnLinuxProcessAndDisown disableCommand+ return True+ else return False
+ src/Seek.hs view
@@ -0,0 +1,93 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++module Seek where++import Control.Monad+import Data.IORef+import Data.Int+import Data.Time.Clock.POSIX+import Data.GI.Base.Signals+import GI.GLib+import GI.GObject+import qualified GI.Gtk+import qualified GI.Gst++import qualified Records as R++addSeekHandlers :: R.Application -> IO ()+addSeekHandlers+ R.Application {+ R.guiObjects = R.GuiObjects {+ R.seekScale = seekScale+ }+ , R.ioRefs = R.IORefs {+ R.videoInfoRef = videoInfoRef+ }+ , R.playbin = playbin+ }+ = do+ seekScaleHandlerId <- GI.Gtk.onRangeValueChanged seekScale (+ onRangeValueChanged+ playbin+ seekScale+ videoInfoRef+ )+ void (+ GI.GLib.timeoutAdd GI.GLib.PRIORITY_DEFAULT 41 (+ updateSeekScale+ playbin+ seekScale+ seekScaleHandlerId+ )+ )++seekRequestLastDeltaThreshold :: R.VideoInfo -> POSIXTime+seekRequestLastDeltaThreshold R.VideoInfo { R.isLocalFile = True } = 0.0+seekRequestLastDeltaThreshold R.VideoInfo { R.isLocalFile = False } = 0.0++onRangeValueChanged ::+ GI.Gst.Element ->+ GI.Gtk.Scale ->+ IORef R.VideoInfo ->+ IO ()+onRangeValueChanged+ playbin+ seekScale+ videoInfoRef+ = 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+ videoInfoGathered <- readIORef videoInfoRef+ -- When playing over a network (not local),+ -- sending too many asynchronous seek requests will cause GStreamer to error.+ -- Block until this seek request finishes which will block the main GTK main thread.+ -- Note that by blocking here, the seek scale will not slide.+ -- The user will have to point and click on the scale track.+ unless (R.isLocalFile videoInfoGathered) $ void $ GI.Gst.elementGetState playbin GI.Gst.CLOCK_TIME_NONE++updateSeekScale ::+ GI.Gst.Element ->+ GI.Gtk.Scale ->+ Data.GI.Base.Signals.SignalHandlerId ->+ IO Bool+updateSeekScale+ playbin+ seekScale+ seekScaleHandlerId+ = do+ (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime+ (couldQueryPosition, position) <- GI.Gst.elementQueryPosition playbin GI.Gst.FormatTime+ when (couldQueryDuration && couldQueryPosition && duration > 0) $ do+ let percentage = 100.0 * (fromIntegral position / fromIntegral duration :: Double)+ GI.GObject.signalHandlerBlock seekScale seekScaleHandlerId+ GI.Gtk.rangeSetValue seekScale percentage+ GI.GObject.signalHandlerUnblock seekScale seekScaleHandlerId+ return True
+ src/Uri.hs view
@@ -0,0 +1,66 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}++module Uri where++import System.FilePath+import Filesystem+import Filesystem.Path.CurrentOS+import Data.Text++removeFileUriScheme :: Prelude.String -> Prelude.String+removeFileUriScheme str =+ if hasFileUriScheme str+ then Data.Text.unpack $ Data.Text.replace "file://" "" uri+ else Data.Text.unpack uri+ where+ uri :: Data.Text.Text+ uri = Data.Text.pack str++hasFileUriScheme :: Prelude.String -> Bool+hasFileUriScheme = hasUriScheme "file://"++hasHttpUriScheme :: Prelude.String -> Bool+hasHttpUriScheme str = isHttp || isHttps+ where+ isHttp :: Bool+ isHttp = hasUriScheme "http://" str+ isHttps :: Bool+ isHttps = hasUriScheme "https://" str++hasUriScheme :: Prelude.String -> Prelude.String -> Bool+hasUriScheme scheme uri = toLower' scheme `Data.Text.isPrefixOf` toLower' uri+ where+ toLower' :: Prelude.String -> Text+ toLower' = Data.Text.toLower . Data.Text.pack++addUriSchemeIfNone :: Prelude.String -> IO Prelude.String+addUriSchemeIfNone filePathName =+ isLocalFile filePathName >>=+ \ local ->+ return $+ if local+ then if hasFileUriScheme filePathName then filePathName else "file://" ++ filePathName+ else if hasHttpUriScheme filePathName then filePathName else "http://" ++ filePathName++isLocalFile :: Prelude.String -> IO Bool+isLocalFile = Filesystem.isFile . Filesystem.Path.CurrentOS.fromText . Data.Text.pack . removeFileUriScheme++toAbsoluteUri :: String -> IO String+toAbsoluteUri uri = do+ cwd <- getWorkingDirectory+ let cwd' = case toText cwd of+ Right x -> x+ Left y -> y+ isLocal <- isLocalFile uri+ if not isLocal+ then return uri+ else+ if isRelative uri+ then return $ normalise $ combine (Data.Text.unpack cwd') uri+ else return uri
+ src/Utils.hs view
@@ -0,0 +1,58 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++{-# LANGUAGE ScopedTypeVariables #-}++module Utils where++import System.Exit+import System.Process+import System.FilePath+import Control.Monad+import Control.Exception+import Data.List+import Data.Text+import Data.Int+import qualified GI.Gtk++fileNameFromFilePathName :: Data.Text.Text -> Data.Text.Text+fileNameFromFilePathName = Data.Text.pack . System.FilePath.takeFileName . Data.Text.unpack++enumToInt32 :: (Enum a, Ord a) => a -> Int32+enumToInt32 enum = fromIntegral (fromEnum enum) :: Int32++isTextEmpty :: Data.Text.Text -> Bool+isTextEmpty = Data.Text.null . Data.Text.strip++getSelectedVideoWidth :: GI.Gtk.ComboBoxText -> IO Int+getSelectedVideoWidth = fmap (\ x -> read (Data.Text.unpack x) :: Int) . GI.Gtk.comboBoxTextGetActiveText++linuxProcessIsRunning :: String -> IO Bool+linuxProcessIsRunning processName = do+ (_, out, _) <- safeRunProcessGetOutput "pgrep" ["-f", processName]+ return (not $ Data.List.null out)++spawnLinuxProcessAndDisown :: String -> IO ()+spawnLinuxProcessAndDisown processCommand =+ void $+ System.Process.spawnCommand+ (+ "nohup "+ ++ processCommand+ ++ " </dev/null &>/dev/null & disown"+ )++safeRunProcessGetOutput :: String -> [String] -> IO (System.Exit.ExitCode, String, String)+safeRunProcessGetOutput processName args =+ catch (+ readProcessWithExitCode+ processName+ args+ ""+ ) (\ (e :: Control.Exception.IOException) ->+ print e >>+ return (ExitFailure 1, "", "")+ )
+ src/VideoInfo.hs view
@@ -0,0 +1,76 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module VideoInfo where++import System.Exit+import Text.Read+import Data.Maybe+import Data.Text+import Data.IORef++import qualified Records as R+import Uri+import Utils++getVideoInfoRaw :: Prelude.String -> IO (Maybe Prelude.String)+getVideoInfoRaw uri = do+ (code, out, _) <- safeRunProcessGetOutput "gst-discoverer-1.0" [uri, "-v"]+ if code == System.Exit.ExitSuccess+ then return (Just out)+ else return Nothing++getVideoInfo :: IORef R.VideoInfo -> Prelude.String -> IO R.VideoInfo+getVideoInfo videoInfoRef filePathName = do+ videoInfoGathered <- readIORef videoInfoRef+ uri <- addUriSchemeIfNone filePathName+ cacheDo (R.uri videoInfoGathered == uri) videoInfoGathered uri+ where+ widthField :: Data.Text.Text+ widthField = "width: "+ heightField :: Data.Text.Text+ heightField = "height: "+ seekableField :: Data.Text.Text+ seekableField = "seekable: "+ cacheDo :: Bool -> R.VideoInfo -> Prelude.String -> IO R.VideoInfo+ cacheDo True videoInfoGathered _ = return videoInfoGathered+ cacheDo False _ uri = do+ videoInfoRaw <- getVideoInfoRaw uri+ processVideoInfoRaw videoInfoRaw uri+ processVideoInfoRaw :: Maybe Prelude.String -> Prelude.String -> IO R.VideoInfo+ processVideoInfoRaw Nothing _ = do+ let videoInfoGathered = R.defaultVideoInfo+ atomicWriteIORef videoInfoRef videoInfoGathered+ return videoInfoGathered+ processVideoInfoRaw (Just str) uri = do+ let text = Data.Text.toLower $ Data.Text.pack str+ let textLines = Data.Text.lines text+ let videoInfoGathered = R.VideoInfo {+ R.uri = uri+ , R.isLocalFile = hasFileUriScheme uri+ , R.isVideo = "video: video/" `Data.Text.isInfixOf` text+ , R.isSeekable = "yes" == getField seekableField textLines+ , R.videoWidth = getDimension R.videoWidth widthField textLines+ , R.videoHeight = getDimension R.videoHeight heightField textLines+ }+ atomicWriteIORef videoInfoRef videoInfoGathered+ return videoInfoGathered+ getDimension :: (R.VideoInfo -> Int) -> Data.Text.Text -> [Data.Text.Text] -> Int+ getDimension f field lines' = fromMaybe (+ f R.defaultVideoInfo+ ) (readMaybe (Data.Text.unpack $ getField field lines') :: Maybe Int)+ getField :: Data.Text.Text -> [Data.Text.Text] -> Data.Text.Text+ getField field =+ Prelude.foldl (\ acc l ->+ if not $ Data.Text.null acc+ then acc+ else if field `Data.Text.isInfixOf` l+ then Data.Text.replace field "" $ Data.Text.strip l+ else ""+ ) ""
+ src/VideoSizeSelector.hs view
@@ -0,0 +1,54 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++module VideoSizeSelector where++import Control.Monad+import Data.Text+import qualified GI.Gtk++import qualified Records as R+import Window+import VideoInfo+import Utils++addVideoSizeSelectorHandler :: R.Application -> IO ()+addVideoSizeSelectorHandler+ application@R.Application {+ R.guiObjects = R.GuiObjects {+ R.videoWidthSelectionComboBox = videoWidthSelectionComboBox+ }+ }+ = void (+ GI.Gtk.onComboBoxChanged+ videoWidthSelectionComboBox+ (videoSizeSelectionHandler application)+ )++videoSizeSelectionHandler ::+ R.Application ->+ IO ()+videoSizeSelectionHandler+ R.Application {+ R.guiObjects = guiObjects@R.GuiObjects {+ R.window = window+ , R.fileChooserButton = fileChooserButton+ , R.videoWidget = videoWidget+ , R.videoWidthSelectionComboBox = videoWidthSelectionComboBox+ , R.fileChooserEntry = fileChooserEntry+ }+ , R.ioRefs = R.IORefs {+ R.videoInfoRef = videoInfoRef+ }+ }+ = do+ filePathName <- Data.Text.unpack <$> GI.Gtk.entryGetText fileChooserEntry+ videoWidthSelection <- getSelectedVideoWidth videoWidthSelectionComboBox+ retrievedVideoInfo <- getVideoInfo videoInfoRef filePathName+ maybeWindowSize <- calculateWindowSize videoWidthSelection retrievedVideoInfo+ case maybeWindowSize of+ Nothing -> resetWindow guiObjects+ Just (width, height) -> setWindowSize width height fileChooserButton videoWidget window
+ src/Window.hs view
@@ -0,0 +1,195 @@+{-+ Movie Monad+ (C) 2017 David lettier+ lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}++module Window where++import GHC.Word+import Control.Monad+import Data.Int+import Data.IORef+import Data.Time.Clock.POSIX+import GI.GLib+import qualified GI.Gdk+import qualified GI.Gtk+import qualified GI.Gst++import qualified Records as R+import Constants+import Mouse+import PlayPause+import Utils++addWindowHandlers :: R.Application -> [R.Application -> IO ()] -> IO ()+addWindowHandlers+ application@R.Application {+ R.guiObjects = R.GuiObjects {+ R.window = window+ , R.videoWidget = videoWidget+ }+ , R.ioRefs = R.IORefs {+ R.isWindowFullScreenRef = isWindowFullScreenRef+ }+ , R.playbin = playbin+ }+ functionsToRunOnWindowRealized+ =+ void (+ GI.Gtk.onWidgetRealize videoWidget (+ windowRealizedHandler+ application+ functionsToRunOnWindowRealized+ )+ ) >>+ void (+ GI.Gtk.onWidgetWindowStateEvent window (widgetWindowStateEventHandler isWindowFullScreenRef)+ ) >>+ void (+ GI.Gtk.onWidgetDestroy window (windowDestroyHandler playbin)+ ) >>+ void (+ GI.GLib.timeoutAddSeconds GI.GLib.PRIORITY_DEFAULT 1 (hideOnScreenControls application)+ )++windowRealizedHandler ::+ R.Application ->+ [R.Application -> IO ()] ->+ GI.Gtk.WidgetRealizeCallback+windowRealizedHandler+ application@R.Application {+ R.guiObjects = guiObjects@R.GuiObjects {+ R.videoWidget = videoWidget+ , R.seekScale = seekScale+ }+ }+ functionsToRunOnWindowRealized+ = do+ let eventMask = enumToInt32 GI.Gdk.EventMaskAllEventsMask+ GI.Gtk.widgetAddEvents videoWidget eventMask+ GI.Gtk.widgetAddEvents seekScale eventMask+ resetWindow guiObjects+ mapM_ (\ f -> f application) functionsToRunOnWindowRealized++widgetWindowStateEventHandler ::+ IORef Bool ->+ GI.Gdk.EventWindowState ->+ IO Bool+widgetWindowStateEventHandler isWindowFullScreenRef eventWindowState = do+ windowStates <- GI.Gdk.getEventWindowStateNewWindowState eventWindowState+ let isWindowFullScreen = Prelude.foldl (\ acc x ->+ acc || GI.Gdk.WindowStateFullscreen == x+ ) False windowStates+ atomicWriteIORef isWindowFullScreenRef isWindowFullScreen+ return True++windowDestroyHandler ::+ GI.Gst.Element ->+ IO ()+windowDestroyHandler playbin = do+ _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull+ _ <- GI.Gst.objectUnref playbin+ GI.Gtk.mainQuit++hideOnScreenControls ::+ R.Application ->+ IO Bool+hideOnScreenControls+ R.Application {+ R.guiObjects = R.GuiObjects {+ R.window = window+ , R.fileChooserButton = fileChooserButton+ , R.bottomControlsGtkBox = bottomControlsGtkBox+ }+ , R.ioRefs = R.IORefs {+ R.videoInfoRef = videoInfoRef+ , R.mouseMovedLastRef = mouseMovedLastRef+ }+ , R.playbin = playbin+ }+ = do+ isVideo <- R.isVideo <$> readIORef videoInfoRef+ mouseMovedLast <- readIORef mouseMovedLastRef+ timeNow <- fmap round getPOSIXTime+ (_, playBinState, _) <- GI.Gst.elementGetState playbin (fromIntegral GI.Gst.MSECOND :: GHC.Word.Word64)+ let isPlaying = GI.Gst.StatePlaying == playBinState+ let delta = timeNow - mouseMovedLast+ when (isPlaying && isVideo && delta >= hideOnScreenControlsInterval) $ do+ GI.Gtk.widgetHide fileChooserButton+ GI.Gtk.widgetHide bottomControlsGtkBox+ setCursor window (Just "none")+ atomicWriteIORef mouseMovedLastRef timeNow+ return True++calculateWindowSize :: Int -> R.VideoInfo -> IO (Maybe (Int32, Int32))+calculateWindowSize videoWidthSelection retrievedVideoInfo =+ widthHeightToDouble retrievedVideoInfo >>=+ ratio >>=+ windowSize+ where+ widthHeightToDouble :: R.VideoInfo -> IO (Maybe Double, Maybe Double)+ widthHeightToDouble R.VideoInfo { R.isVideo = False } = return (Nothing, Nothing)+ widthHeightToDouble R.VideoInfo { R.videoWidth = w, R.videoHeight = h } =+ return (Just $ fromIntegral w :: Maybe Double, Just $ fromIntegral h :: Maybe Double)+ ratio :: (Maybe Double, Maybe Double) -> IO (Maybe Double)+ ratio (Just width, Just height) =+ if width <= 0.0 then return Nothing else return (Just (height / width))+ ratio _ = return Nothing+ windowSize :: Maybe Double -> IO (Maybe (Int32, Int32))+ windowSize Nothing = return Nothing+ windowSize (Just ratio') =+ return (+ Just (+ fromIntegral videoWidthSelection :: Int32+ , round ((fromIntegral videoWidthSelection :: Double) * ratio') :: Int32+ )+ )++setWindowSize ::+ Int32 ->+ Int32 ->+ GI.Gtk.Button ->+ GI.Gtk.Widget ->+ GI.Gtk.Window ->+ IO ()+setWindowSize width height fileChooserButton videoWidget window = do+ GI.Gtk.setWidgetWidthRequest fileChooserButton width+ GI.Gtk.setWidgetWidthRequest videoWidget width+ GI.Gtk.setWidgetHeightRequest videoWidget height+ GI.Gtk.setWidgetWidthRequest window width+ GI.Gtk.setWidgetHeightRequest window height+ GI.Gtk.windowResize window width (if height <= 0 then 1 else height)++resetWindow ::+ R.GuiObjects ->+ IO ()+resetWindow+ R.GuiObjects {+ R.window = window+ , R.fileChooserButton = fileChooserButton+ , R.videoWidget = videoWidget+ , R.seekScale = seekScale+ , R.playPauseButton = playPauseButton+ , R.videoWidthSelectionComboBox = videoWidthSelectionComboBox+ , R.fullscreenButton = fullscreenButton+ , R.playImage = playImage+ , R.pauseImage = pauseImage+ , R.bottomControlsGtkBox = bottomControlsGtkBox+ }+ = do+ videoWidthSelection <- getSelectedVideoWidth videoWidthSelectionComboBox+ let width = fromIntegral videoWidthSelection :: Int32+ GI.Gtk.windowUnfullscreen window+ GI.Gtk.widgetHide videoWidget+ GI.Gtk.widgetHide seekScale+ GI.Gtk.widgetHide playPauseButton+ GI.Gtk.widgetHide fullscreenButton+ GI.Gtk.widgetShow fileChooserButton+ GI.Gtk.widgetShow bottomControlsGtkBox+ GI.Gtk.widgetShow videoWidthSelectionComboBox+ setCursor window Nothing+ setPlayPauseButton playPauseButton playImage pauseImage False+ setWindowSize width 0 fileChooserButton videoWidget window
src/data/gui.glade view
@@ -1,66 +1,165 @@ <?xml version="1.0" encoding="UTF-8"?>-<!-- Generated with glade 3.20.0 -->+<!-- Generated with glade 3.20.0 ++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 <organization> 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 <COPYRIGHT HOLDER> 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.++Author: David Lettier++--> <interface>- <requires lib="gtk+" version="3.20"/>+ <requires lib="gtk+" version="3.10"/>+ <!-- interface-license-type bsd3c -->+ <!-- interface-name Movie Monad -->+ <!-- interface-description A desktop video player built with Haskell that uses GStreamer and GTK+. -->+ <!-- interface-copyright 2017 David Lettier -->+ <!-- interface-authors David Lettier --> <object class="GtkImage" id="about-image"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-dialog-question</property> </object>- <object class="GtkAdjustment" id="adjustment1">- <property name="upper">100</property>- <property name="step_increment">1</property>- <property name="page_increment">10</property>+ <object class="GtkButton" id="file-chooser-cancel-button">+ <property name="label">gtk-cancel</property>+ <property name="visible">True</property>+ <property name="can_focus">True</property>+ <property name="receives_default">True</property>+ <property name="use_stock">True</property>+ <property name="always_show_image">True</property> </object>+ <object class="GtkButton" id="file-chooser-open-button">+ <property name="label">gtk-open</property>+ <property name="visible">True</property>+ <property name="can_focus">True</property>+ <property name="receives_default">True</property>+ <property name="use_stock">True</property>+ <property name="always_show_image">True</property>+ </object> <object class="GtkImage" id="fullscreen-image"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-fullscreen</property> </object>- <object class="GtkWindow" id="window">+ <object class="GtkImage" id="pause-image">+ <property name="visible">True</property> <property name="can_focus">False</property>+ <property name="stock">gtk-media-pause</property>+ </object>+ <object class="GtkImage" id="play-image">+ <property name="visible">True</property>+ <property name="can_focus">False</property>+ <property name="stock">gtk-media-play</property>+ </object>+ <object class="GtkAdjustment" id="seek-scale-adjustment">+ <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">True</property> <property name="title" translatable="yes">Movie Monad</property> <property name="window_position">center</property>- <property name="icon_name">applications-multimedia</property>+ <property name="icon">movie-monad-icon.png</property> <property name="gravity">center</property> <child>- <object class="GtkBox">+ <object class="GtkBox" id="box-1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="orientation">vertical</property> <child>- <object class="GtkBox">+ <object class="GtkBox" id="box-2"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="hexpand">True</property> <property name="vexpand">True</property> <property name="orientation">vertical</property> <child>- <object class="GtkFileChooserButton" id="file-chooser-button">+ <object class="GtkButton" id="file-chooser-button"> <property name="width_request">800</property> <property name="visible">True</property> <property name="app_paintable">True</property>- <property name="can_focus">False</property>- <property name="tooltip_text" translatable="yes">Select a video file.</property>+ <property name="can_focus">True</property>+ <property name="receives_default">True</property>+ <property name="tooltip_text" translatable="yes">Open a video file</property> <property name="valign">start</property> <property name="hexpand">True</property>- <property name="create_folders">False</property>- <property name="title" translatable="yes">Movie Monad</property>+ <property name="relief">half</property>+ <property name="image_position">right</property>+ <property name="always_show_image">True</property>+ <child>+ <object class="GtkBox" id="box-3">+ <property name="visible">True</property>+ <property name="can_focus">False</property>+ <child>+ <object class="GtkLabel" id="file-chooser-button-label">+ <property name="visible">True</property>+ <property name="can_focus">False</property>+ <property name="label" translatable="yes">Open</property>+ <property name="justify">fill</property>+ <property name="ellipsize">end</property>+ <property name="single_line_mode">True</property>+ </object>+ <packing>+ <property name="expand">True</property>+ <property name="fill">True</property>+ <property name="position">0</property>+ </packing>+ </child>+ <child>+ <object class="GtkImage" id="open-image">+ <property name="visible">True</property>+ <property name="can_focus">False</property>+ <property name="stock">gtk-open</property>+ </object>+ <packing>+ <property name="expand">False</property>+ <property name="fill">False</property>+ <property name="pack_type">end</property>+ <property name="position">2</property>+ </packing>+ </child>+ </object>+ </child> </object> <packing> <property name="expand">False</property>- <property name="fill">True</property>+ <property name="fill">False</property> <property name="position">0</property> </packing> </child> <child>- <object class="GtkDrawingArea" id="drawing-area">+ <object class="GtkBox" id="video-widget-box"> <property name="visible">True</property>- <property name="sensitive">False</property> <property name="app_paintable">True</property>- <property name="can_focus">False</property>+ <property name="can_focus">True</property> <property name="hexpand">True</property> <property name="vexpand">True</property>+ <property name="orientation">vertical</property>+ <child>+ <placeholder/>+ </child> </object> <packing> <property name="expand">True</property>@@ -69,163 +168,209 @@ </packing> </child> <child>- <object class="GtkScale" id="seek-scale">+ <object class="GtkBox" id="bottom-controls-gtk-box"> <property name="visible">True</property>- <property name="can_focus">True</property>+ <property name="can_focus">False</property> <property name="valign">end</property>- <property name="margin_left">5</property>- <property name="margin_right">5</property>- <property name="hexpand">True</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">False</property>- <property name="fill">False</property>- <property name="position">2</property>- </packing>- </child>- </object>- <packing>- <property name="expand">True</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="valign">end</property>- <property name="margin_bottom">5</property>- <property name="hexpand">True</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+ <property name="orientation">vertical</property>+ <property name="baseline_position">bottom</property>+ <child>+ <object class="GtkScale" id="seek-scale">+ <property name="visible">True</property>+ <property name="app_paintable">True</property>+ <property name="can_focus">False</property>+ <property name="valign">end</property>+ <property name="margin_left">5</property>+ <property name="margin_right">5</property>+ <property name="hexpand">True</property>+ <property name="adjustment">seek-scale-adjustment</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">False</property>+ <property name="fill">False</property>+ <property name="position">0</property>+ </packing>+ </child>+ <child>+ <object class="GtkBox" id="box-4">+ <property name="visible">True</property>+ <property name="can_focus">False</property>+ <property name="valign">end</property>+ <property name="margin_top">5</property>+ <property name="margin_bottom">5</property>+ <property name="hexpand">True</property>+ <child>+ <object class="GtkButton" id="play-pause-button">+ <property name="visible">True</property>+ <property name="can_focus">True</property>+ <property name="receives_default">True</property>+ <property name="tooltip_text" translatable="yes">Click to play</property>+ <property name="margin_left">5</property>+ <property name="margin_right">5</property>+ <property name="image">play-image</property>+ <property name="always_show_image">True</property>+ </object>+ <packing>+ <property name="expand">False</property>+ <property name="fill">True</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">half</property>+ <property name="orientation">vertical</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>+ <child internal-child="plus_button">+ <object class="GtkButton" id="volume-up-button">+ <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" id="volume-down-button">+ <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="video-width-selection-combo-box">+ <property name="visible">True</property>+ <property name="app_paintable">True</property>+ <property name="can_focus">False</property>+ <property name="tooltip_text" translatable="yes">Set the video width</property>+ <property name="margin_left">5</property>+ <property name="margin_right">5</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="fullscreen-button">+ <property name="app_paintable">True</property>+ <property name="can_focus">False</property>+ <property name="receives_default">False</property>+ <property name="tooltip_text" translatable="yes">Toggle on or off fullscreen</property>+ <property name="margin_left">5</property>+ <property name="margin_right">5</property>+ <property name="image">fullscreen-image</property>+ <property name="relief">half</property>+ <property name="always_show_image">True</property>+ </object>+ <packing>+ <property name="expand">False</property>+ <property name="fill">True</property>+ <property name="position">3</property>+ </packing>+ </child>+ <child>+ <object class="GtkSpinner" id="buffering-spinner">+ <property name="sensitive">False</property>+ <property name="app_paintable">True</property>+ <property name="can_focus">False</property>+ <property name="margin_left">5</property>+ </object>+ <packing>+ <property name="expand">False</property>+ <property name="fill">False</property>+ <property name="position">4</property>+ </packing>+ </child>+ <child>+ <object class="GtkButton" id="about-button">+ <property name="visible">True</property>+ <property name="app_paintable">True</property>+ <property name="can_focus">False</property>+ <property name="receives_default">False</property>+ <property name="tooltip_text" translatable="yes">About Movie Monad</property>+ <property name="margin_left">5</property>+ <property name="margin_right">5</property>+ <property name="image">about-image</property>+ <property name="relief">half</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">5</property>+ </packing>+ </child> </object>+ <packing>+ <property name="expand">False</property>+ <property name="fill">False</property>+ <property name="position">1</property>+ </packing> </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="visible">True</property>- <property name="can_focus">True</property>- <property name="receives_default">True</property>- <property name="margin_right">5</property>- <property name="image">about-image</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>- <child>- <object class="GtkButton" id="fullscreen-button">- <property name="can_focus">True</property>- <property name="receives_default">True</property>- <property name="tooltip_text" translatable="yes">Toggle on or off fullscreen.</property>- <property name="image">fullscreen-image</property>- <property name="always_show_image">True</property>- </object>- <packing>- <property name="expand">False</property>- <property name="fill">True</property>- <property name="position">4</property>- </packing>- </child> </object> <packing>- <property name="expand">False</property>- <property name="fill">False</property>- <property name="position">1</property>+ <property name="expand">True</property>+ <property name="fill">True</property>+ <property name="position">0</property> </packing> </child> </object>@@ -236,7 +381,7 @@ </object> <object class="GtkAboutDialog" id="about-dialog"> <property name="can_focus">False</property>- <property name="title" translatable="yes">Movie Monad</property>+ <property name="title" translatable="yes">Movie Monad - About</property> <property name="window_position">center</property> <property name="icon_name">dialog-information</property> <property name="type_hint">dialog</property>@@ -244,8 +389,9 @@ <property name="transient_for">window</property> <property name="attached_to">window</property> <property name="program_name">Movie Monad</property>- <property name="version">0.0.1.0</property>- <property name="copyright" translatable="yes">(C) 2017 David Lettier</property>+ <property name="version">0.0.2.0</property>+ <property name="copyright" translatable="yes">(C) 2017 David Lettier+lettier.com</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@@ -278,15 +424,15 @@ 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="logo">movie-monad-logo.svg</property> <property name="license_type">custom</property> <child internal-child="vbox">- <object class="GtkBox">+ <object class="GtkBox" id="box-5"> <property name="can_focus">False</property> <property name="orientation">vertical</property> <property name="spacing">2</property> <child internal-child="action_area">- <object class="GtkButtonBox">+ <object class="GtkButtonBox" id="about-dialog-button-box"> <property name="can_focus">False</property> <property name="layout_style">end</property> </object>@@ -296,9 +442,6 @@ <property name="position">0</property> </packing> </child>- <child>- <placeholder/>- </child> </object> </child> <child>@@ -307,7 +450,7 @@ </object> <object class="GtkMessageDialog" id="error-message-dialog"> <property name="can_focus">False</property>- <property name="title" translatable="yes">Movie Monad</property>+ <property name="title" translatable="yes">Movie Monad - Error</property> <property name="window_position">center</property> <property name="destroy_with_parent">True</property> <property name="icon_name">dialog-error</property>@@ -319,15 +462,17 @@ <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>+ <property name="secondary_text">If you feel like this is a bug, please visit <a href="https://github.com/lettier/movie-monad/issues">the issue page</a> to file a report.++Thank you.</property>+ <property name="secondary_use_markup">True</property> <child internal-child="vbox">- <object class="GtkBox">+ <object class="GtkBox" id="box-6"> <property name="can_focus">False</property> <property name="orientation">vertical</property> <property name="spacing">2</property> <child internal-child="action_area">- <object class="GtkButtonBox">+ <object class="GtkButtonBox" id="error-message-dialog-action-area"> <property name="can_focus">False</property> <property name="homogeneous">True</property> <property name="layout_style">expand</property>@@ -336,6 +481,73 @@ <property name="expand">False</property> <property name="fill">False</property> <property name="position">0</property>+ </packing>+ </child>+ </object>+ </child>+ <child>+ <placeholder/>+ </child>+ </object>+ <object class="GtkDialog" id="file-chooser-dialog">+ <property name="can_focus">False</property>+ <property name="title" translatable="yes">Movie Monad - Open a Video File</property>+ <property name="type_hint">dialog</property>+ <property name="transient_for">window</property>+ <child internal-child="vbox">+ <object class="GtkBox" id="box-7">+ <property name="can_focus">False</property>+ <property name="orientation">vertical</property>+ <property name="spacing">2</property>+ <property name="baseline_position">top</property>+ <child internal-child="action_area">+ <object class="GtkButtonBox" id="file-chooser-dialog-action-area">+ <property name="can_focus">False</property>+ <property name="baseline_position">bottom</property>+ <property name="layout_style">end</property>+ <child>+ <placeholder/>+ </child>+ <child>+ <placeholder/>+ </child>+ </object>+ <packing>+ <property name="expand">False</property>+ <property name="fill">False</property>+ <property name="position">0</property>+ </packing>+ </child>+ <child>+ <object class="GtkEntry" id="file-chooser-entry">+ <property name="visible">True</property>+ <property name="can_focus">True</property>+ <property name="tooltip_text" translatable="yes">Type in the path to a local file or URL</property>+ <property name="has_frame">False</property>+ <property name="truncate_multiline">True</property>+ <property name="primary_icon_stock">gtk-edit</property>+ <property name="secondary_icon_stock">gtk-clear</property>+ <property name="primary_icon_activatable">False</property>+ <property name="primary_icon_sensitive">False</property>+ <property name="secondary_icon_tooltip_text" translatable="yes">Clear the video location</property>+ <property name="placeholder_text" translatable="yes">Video Location</property>+ </object>+ <packing>+ <property name="expand">False</property>+ <property name="fill">True</property>+ <property name="position">0</property>+ </packing>+ </child>+ <child>+ <object class="GtkFileChooserWidget" id="file-chooser-widget">+ <property name="visible">True</property>+ <property name="can_focus">False</property>+ <property name="create_folders">False</property>+ </object>+ <packing>+ <property name="expand">False</property>+ <property name="fill">True</property>+ <property name="position">1</property> </packing> </child> </object>
+ src/data/movie-monad-icon.png view
binary file changed (absent → 24223 bytes)
+ src/data/movie-monad-logo.svg view
@@ -0,0 +1,768 @@+<?xml version="1.0" encoding="UTF-8" standalone="no"?>+<!-- Created with Inkscape (http://www.inkscape.org/) -->++<svg+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"+ xmlns:dc="http://purl.org/dc/elements/1.1/"+ xmlns:cc="http://creativecommons.org/ns#"+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"+ xmlns:svg="http://www.w3.org/2000/svg"+ xmlns="http://www.w3.org/2000/svg"+ xmlns:xlink="http://www.w3.org/1999/xlink"+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"+ width="200mm"+ height="100mm"+ viewBox="0 0 200 100"+ version="1.1"+ id="svg8"+ inkscape:version="0.92.1 r"+ sodipodi:docname="movie-monad-logo.svg"+ inkscape:export-filename="/home/lettier/tmp/movie-monad-logo.png"+ inkscape:export-xdpi="88.900002"+ inkscape:export-ydpi="88.900002">+ <defs+ id="defs2">+ <inkscape:path-effect+ effect="bend_path"+ id="path-effect10605"+ is_visible="true"+ bendpath="m 424.1184,110.88365 c 102.124,-4.64842 205.00309,-10.80704 299.39935,0"+ prop_scale="1"+ scale_y_rel="false"+ vertical="false"+ bendpath-nodetypes="cc" />+ <inkscape:path-effect+ effect="bounding_box"+ id="path-effect10603"+ is_visible="true"+ linkedpath=""+ visualbounds="false" />+ <inkscape:path-effect+ effect="fill_between_many"+ id="path-effect10601"+ is_visible="true"+ linkedpaths="" />+ <inkscape:path-effect+ effect="rough_hatches"+ id="path-effect10599"+ is_visible="true"+ direction="573.81807,105.88365 , 74.849838,0"+ dist_rdm="75;1"+ growth="0"+ do_bend="true"+ bender="573.81807,110.88365 , 5,0"+ bottom_edge_variation="13.03522;1"+ top_edge_variation="13.03522;1"+ bottom_tgt_variation="0;1"+ top_tgt_variation="0;1"+ scale_bf="1"+ scale_bb="1"+ scale_tf="1"+ scale_tb="1"+ top_smth_variation="0;1"+ bottom_smth_variation="0;1"+ fat_output="true"+ stroke_width_top="1"+ stroke_width_bottom="1"+ front_thickness="1"+ back_thickness="0.25" />+ <inkscape:path-effect+ effect="powerstroke"+ id="path-effect10597"+ is_visible="true"+ offset_points="0.2,1.5 | 1.5,1.5 | 2.8,1.5"+ sort_points="true"+ interpolator_type="CubicBezierFit"+ interpolator_beta="0.81"+ start_linecap_type="peak"+ linejoin_type="extrp_arc"+ miter_limit="4"+ end_linecap_type="butt" />+ <inkscape:path-effect+ effect="roughen"+ id="path-effect10595"+ is_visible="true"+ method="segments"+ max_segment_size="10"+ segments="2"+ displace_x="10;1"+ displace_y="10;1"+ global_randomize="1;1"+ handles="along"+ shift_nodes="true"+ fixed_displacement="false"+ spray_tool_friendly="false" />+ <inkscape:path-effect+ effect="simplify"+ id="path-effect10593"+ is_visible="true"+ steps="0"+ threshold="0.002"+ smooth_angles="0"+ helper_size="5"+ simplify_individual_paths="false"+ simplify_just_coalesce="false" />+ <marker+ inkscape:stockid="Arrow1Mend"+ orient="auto"+ refY="0.0"+ refX="0.0"+ id="Arrow1Mend"+ style="overflow:visible;"+ inkscape:isstock="true">+ <path+ id="path10335"+ d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "+ style="fill-rule:evenodd;stroke:#57ace3;stroke-width:1pt;stroke-opacity:1;fill:#57ace3;fill-opacity:1"+ transform="scale(0.4) rotate(180) translate(10,0)" />+ </marker>+ <marker+ inkscape:stockid="Arrow1Lstart"+ orient="auto"+ refY="0.0"+ refX="0.0"+ id="marker10589"+ style="overflow:visible"+ inkscape:isstock="true">+ <path+ id="path10326"+ d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "+ style="fill-rule:evenodd;stroke:#57ace3;stroke-width:1pt;stroke-opacity:1;fill:#57ace3;fill-opacity:1"+ transform="scale(0.8) translate(12.5,0)" />+ </marker>+ <inkscape:path-effect+ tgtlength_rdm="1;499169899"+ tgtlength="60"+ tgtscale="5"+ tgt_places_rdmness="1;2340095931"+ nbtangents="3"+ tremble_frequency="0.01"+ tremble_size="1;2147483646"+ parallel_offset="0;618914966"+ ends_tolerance="1;1"+ strokeoverlap_rdm="1;1527224402"+ strokeoverlap="1"+ strokelength_rdm="1;693460889"+ strokelength="100"+ nbiter_approxstrokes="2"+ is_visible="true"+ id="path-effect10204"+ effect="sketch" />+ <linearGradient+ id="linearGradient10200"+ inkscape:collect="always">+ <stop+ id="stop10196"+ offset="0"+ style="stop-color:#57ace3;stop-opacity:1" />+ <stop+ id="stop10198"+ offset="1"+ style="stop-color:#e0437f;stop-opacity:0.89019608" />+ </linearGradient>+ <inkscape:path-effect+ effect="taper_stroke"+ id="path-effect10194"+ is_visible="true"+ stroke_width="5"+ attach_start="0.99999"+ end_offset="0.99999"+ smoothing="0.1"+ jointype="bevel"+ miter_limit="100" />+ <inkscape:path-effect+ effect="taper_stroke"+ id="path-effect10171"+ is_visible="true"+ stroke_width="1"+ attach_start="0.2"+ end_offset="0.2"+ smoothing="0.5"+ jointype="extrapolated"+ miter_limit="100" />+ <inkscape:path-effect+ effect="simplify"+ id="path-effect10169"+ is_visible="true"+ steps="19"+ threshold="0.002"+ smooth_angles="0"+ helper_size="5"+ simplify_individual_paths="false"+ simplify_just_coalesce="false" />+ <inkscape:path-effect+ effect="roughen"+ id="path-effect10167"+ is_visible="true"+ method="segments"+ max_segment_size="9"+ segments="2"+ displace_x="1;1"+ displace_y="1;1"+ global_randomize="1;1"+ handles="along"+ shift_nodes="true"+ fixed_displacement="false"+ spray_tool_friendly="false" />+ <inkscape:path-effect+ effect="powerstroke"+ id="path-effect10165"+ is_visible="true"+ offset_points="7.5,0.13229166"+ sort_points="true"+ interpolator_type="CubicBezierFit"+ interpolator_beta="0.24"+ start_linecap_type="butt"+ linejoin_type="extrp_arc"+ miter_limit="4"+ end_linecap_type="butt" />+ <inkscape:path-effect+ effect="bend_path"+ id="path-effect10163"+ is_visible="true"+ bendpath="M 6.8171912,228.1058 H 126.12427"+ prop_scale="1"+ scale_y_rel="false"+ vertical="false" />+ <inkscape:path-effect+ effect="construct_grid"+ id="path-effect10161"+ is_visible="true"+ nr_x="5"+ nr_y="5" />+ <inkscape:path-effect+ effect="rough_hatches"+ id="path-effect10159"+ is_visible="true"+ direction="66.722547,223.63972 , 30.669258,0"+ dist_rdm="75;1"+ growth="0"+ do_bend="true"+ bender="66.722547,228.63972 , 5,0"+ bottom_edge_variation="4.25;1"+ top_edge_variation="4.0490918;1"+ bottom_tgt_variation="0;1"+ top_tgt_variation="0;1"+ scale_bf="1"+ scale_bb="1"+ scale_tf="1"+ scale_tb="1"+ top_smth_variation="0;1"+ bottom_smth_variation="0;1"+ fat_output="true"+ stroke_width_top="1"+ stroke_width_bottom="1"+ front_thickness="1"+ back_thickness="0.25" />+ <inkscape:path-effect+ effect="sketch"+ id="path-effect10157"+ is_visible="true"+ nbiter_approxstrokes="5"+ strokelength="10"+ strokelength_rdm="0.3;1264792834"+ strokeoverlap="0.3"+ strokeoverlap_rdm="0.3;1"+ ends_tolerance="0.1;1"+ parallel_offset="1;1"+ tremble_size="1;1"+ tremble_frequency="0.01"+ nbtangents="5"+ tgt_places_rdmness="1;1"+ tgtscale="10"+ tgtlength="5"+ tgtlength_rdm="0.3;1" />+ <inkscape:path-effect+ effect="sketch"+ id="path-effect10155"+ is_visible="true"+ nbiter_approxstrokes="9"+ strokelength="20"+ strokelength_rdm="0.3;1580765317"+ strokeoverlap="0"+ strokeoverlap_rdm="0;797676426"+ ends_tolerance="0;84877062"+ parallel_offset="0.1;2147483646"+ tremble_size="2;1740572465"+ tremble_frequency="1"+ nbtangents="4"+ tgt_places_rdmness="1;1"+ tgtscale="10"+ tgtlength="10"+ tgtlength_rdm="0.3;1" />+ <inkscape:path-effect+ effect="sketch"+ id="path-effect10153"+ is_visible="true"+ nbiter_approxstrokes="1"+ strokelength="100"+ strokelength_rdm="1;693460889"+ strokeoverlap="1"+ strokeoverlap_rdm="1;1527224402"+ ends_tolerance="1;1"+ parallel_offset="1;618914966"+ tremble_size="3;2147483646"+ tremble_frequency="0.01"+ nbtangents="3"+ tgt_places_rdmness="1;2340095931"+ tgtscale="5"+ tgtlength="60"+ tgtlength_rdm="1;499169899" />+ <inkscape:path-effect+ effect="taper_stroke"+ id="path-effect9006"+ is_visible="true"+ stroke_width="20"+ attach_start="0.99999"+ end_offset="3.99999"+ smoothing="0.5"+ jointype="extrapolated"+ miter_limit="200" />+ <inkscape:path-effect+ effect="roughen"+ id="path-effect9004"+ is_visible="true"+ method="segments"+ max_segment_size="10"+ segments="2"+ displace_x="10;1"+ displace_y="10;1"+ global_randomize="1;1"+ handles="along"+ shift_nodes="true"+ fixed_displacement="false"+ spray_tool_friendly="false" />+ <inkscape:path-effect+ effect="spiro"+ id="path-effect9002"+ is_visible="true" />+ <inkscape:path-effect+ effect="sketch"+ id="path-effect9000"+ is_visible="true"+ nbiter_approxstrokes="3"+ strokelength="500"+ strokelength_rdm="0.3;2971216357"+ strokeoverlap="0.3"+ strokeoverlap_rdm="0.3;2083033150"+ ends_tolerance="0.1;1404504716"+ parallel_offset="5;4166125628"+ tremble_size="5;1181147172"+ tremble_frequency="1"+ nbtangents="20"+ tgt_places_rdmness="1;296553611"+ tgtscale="5"+ tgtlength="100"+ tgtlength_rdm="0.3;1794606228" />+ <inkscape:path-effect+ effect="ruler"+ id="path-effect8998"+ is_visible="true"+ unit="px"+ mark_distance="20"+ mark_length="14"+ minor_mark_length="7"+ major_mark_steps="5"+ shift="0"+ offset="0"+ mark_dir="left"+ border_marks="both" />+ <inkscape:path-effect+ effect="simplify"+ id="path-effect8996"+ is_visible="true"+ steps="29"+ threshold="0.002"+ smooth_angles="0"+ helper_size="5"+ simplify_individual_paths="false"+ simplify_just_coalesce="false" />+ <inkscape:path-effect+ effect="show_handles"+ id="path-effect8994"+ is_visible="true"+ nodes="true"+ handles="true"+ original_path="true"+ scale_nodes_and_handles="10" />+ <inkscape:path-effect+ effect="sketch"+ id="path-effect8992"+ is_visible="true"+ nbiter_approxstrokes="1"+ strokelength="20"+ strokelength_rdm="1;1"+ strokeoverlap="1"+ strokeoverlap_rdm="1;1"+ ends_tolerance="0.1;1"+ parallel_offset="0;1"+ tremble_size="5;1"+ tremble_frequency="1"+ nbtangents="20"+ tgt_places_rdmness="1;1"+ tgtscale="10"+ tgtlength="50"+ tgtlength_rdm="0.3;1" />+ <inkscape:path-effect+ effect="powerstroke"+ id="path-effect8990"+ is_visible="true"+ offset_points="3,0.25"+ sort_points="true"+ interpolator_type="Linear"+ interpolator_beta="0.2"+ start_linecap_type="butt"+ linejoin_type="extrp_arc"+ miter_limit="4"+ end_linecap_type="butt" />+ <inkscape:path-effect+ effect="fill_between_strokes"+ id="path-effect8988"+ is_visible="true"+ linkedpath=""+ secondpath=""+ reversesecond="false" />+ <inkscape:path-effect+ effect="fill_between_many"+ id="path-effect8986"+ is_visible="true"+ linkedpaths="" />+ <inkscape:path-effect+ effect="gears"+ id="path-effect8984"+ is_visible="true"+ teeth="10"+ phi="5"+ min_radius="5" />+ <inkscape:path-effect+ effect="rough_hatches"+ id="path-effect8982"+ is_visible="true"+ direction="317.46248,263.61816 , 144.59689,0"+ dist_rdm="75;1924315346"+ growth="0"+ do_bend="true"+ bender="317.46248,268.61816 , 5,0"+ bottom_edge_variation="13.811914;896150574"+ top_edge_variation="13.811914;2070401107"+ bottom_tgt_variation="0;815658193"+ top_tgt_variation="0;1753223962"+ scale_bf="1"+ scale_bb="1"+ scale_tf="1"+ scale_tb="1"+ top_smth_variation="0;1"+ bottom_smth_variation="0;1"+ fat_output="true"+ stroke_width_top="1"+ stroke_width_bottom="1"+ front_thickness="1"+ back_thickness="0.25" />+ <inkscape:path-effect+ effect="construct_grid"+ id="path-effect8980"+ is_visible="true"+ nr_x="5"+ nr_y="5" />+ <linearGradient+ id="linearGradient8935"+ osb:paint="solid">+ <stop+ style="stop-color:#373737;stop-opacity:1;"+ offset="0"+ id="stop8933" />+ </linearGradient>+ <linearGradient+ inkscape:collect="always"+ id="linearGradient5282">+ <stop+ style="stop-color:#814dd2;stop-opacity:1;"+ offset="0"+ id="stop5278" />+ <stop+ style="stop-color:#b46bd3;stop-opacity:0.89019608"+ offset="1"+ id="stop5280" />+ </linearGradient>+ <marker+ inkscape:stockid="Arrow1Lend"+ orient="auto"+ refY="0"+ refX="0"+ id="Arrow1Lend"+ style="overflow:visible"+ inkscape:isstock="true">+ <path+ id="path4997"+ d="M 0,0 5,-5 -12.5,0 5,5 Z"+ style="fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt"+ transform="matrix(-0.8,0,0,-0.8,-10,0)"+ inkscape:connector-curvature="0" />+ </marker>+ <marker+ inkscape:stockid="Arrow1Lstart"+ orient="auto"+ refY="0"+ refX="0"+ id="Arrow1Lstart"+ style="overflow:visible"+ inkscape:isstock="true">+ <path+ id="path4994"+ d="M 0,0 5,-5 -12.5,0 5,5 Z"+ style="fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt"+ transform="matrix(0.8,0,0,0.8,10,0)"+ inkscape:connector-curvature="0" />+ </marker>+ <linearGradient+ inkscape:collect="always"+ xlink:href="#linearGradient5282"+ id="linearGradient5284"+ x1="-119.13808"+ y1="53.642838"+ x2="-64.290207"+ y2="53.285694"+ gradientUnits="userSpaceOnUse" />+ <linearGradient+ inkscape:collect="always"+ xlink:href="#linearGradient5282"+ id="linearGradient5300"+ gradientUnits="userSpaceOnUse"+ x1="-119.13808"+ y1="53.642838"+ x2="-58.218777"+ y2="55.071407" />+ <linearGradient+ inkscape:collect="always"+ xlink:href="#linearGradient5282"+ id="linearGradient5352"+ gradientUnits="userSpaceOnUse"+ x1="-119.13808"+ y1="53.642838"+ x2="-58.218777"+ y2="55.071407" />+ <linearGradient+ inkscape:collect="always"+ xlink:href="#linearGradient5282"+ id="linearGradient5354"+ gradientUnits="userSpaceOnUse"+ x1="-119.13808"+ y1="53.642838"+ x2="-64.290207"+ y2="53.285694" />+ <linearGradient+ inkscape:collect="always"+ xlink:href="#linearGradient5282"+ id="linearGradient4670"+ gradientUnits="userSpaceOnUse"+ x1="-119.13808"+ y1="53.642838"+ x2="-64.290207"+ y2="53.285694" />+ <linearGradient+ inkscape:collect="always"+ xlink:href="#linearGradient5282"+ id="linearGradient4678"+ gradientUnits="userSpaceOnUse"+ x1="-119.13808"+ y1="53.642838"+ x2="-64.290207"+ y2="53.285694" />+ <linearGradient+ inkscape:collect="always"+ xlink:href="#linearGradient10200"+ id="linearGradient10134"+ gradientUnits="userSpaceOnUse"+ x1="109.73602"+ y1="227.8952"+ x2="192.22939"+ y2="227.8952"+ gradientTransform="matrix(3.7865563,-0.0047396,0.01655021,3.7724911,0.1767767,-743.59466)" />+ </defs>+ <sodipodi:namedview+ id="base"+ pagecolor="#ffffff"+ bordercolor="#666666"+ borderopacity="1.0"+ inkscape:pageopacity="0"+ inkscape:pageshadow="2"+ inkscape:zoom="1"+ inkscape:cx="150.91612"+ inkscape:cy="299.37266"+ inkscape:document-units="mm"+ inkscape:current-layer="layer1"+ showgrid="false"+ showguides="false"+ inkscape:guide-bbox="true"+ inkscape:window-width="1920"+ inkscape:window-height="1156"+ inkscape:window-x="0"+ inkscape:window-y="344"+ inkscape:window-maximized="1"+ inkscape:snap-global="false">+ <sodipodi:guide+ position="43.418068,28.252304"+ orientation="1,0"+ id="guide5364"+ inkscape:locked="false" />+ <sodipodi:guide+ position="7.4001037,11.264559"+ orientation="1,0"+ id="guide4628"+ inkscape:locked="false" />+ <sodipodi:guide+ position="76.491454,48.978922"+ orientation="1,0"+ id="guide4630"+ inkscape:locked="false" />+ <sodipodi:guide+ position="86.775231,47.758654"+ orientation="1,0"+ id="guide4645"+ inkscape:locked="false" />+ <sodipodi:guide+ position="95.872541,45.692643"+ orientation="1,0"+ id="guide4647"+ inkscape:locked="false" />+ <sodipodi:guide+ position="119.3082,86.310238"+ orientation="0,1"+ id="guide4649"+ inkscape:locked="false" />+ <sodipodi:guide+ position="87.006575,49.298283"+ orientation="0,1"+ id="guide4669"+ inkscape:locked="false" />+ <sodipodi:guide+ position="104.75846,51.385422"+ orientation="1,0"+ id="guide4671"+ inkscape:locked="false" />+ <sodipodi:guide+ position="41.353548,11.513509"+ orientation="0,1"+ id="guide4673"+ inkscape:locked="false" />+ <sodipodi:guide+ position="86.73606,46.345371"+ orientation="0,1"+ id="guide4675"+ inkscape:locked="false" />+ <sodipodi:guide+ position="38.89398,84.137282"+ orientation="0,1"+ id="guide4677"+ inkscape:locked="false" />+ <sodipodi:guide+ position="102.68449,51.22072"+ orientation="1,0"+ id="guide4693"+ inkscape:locked="false" />+ <inkscape:grid+ type="xygrid"+ id="grid4698" />+ <sodipodi:guide+ position="159.87448,27.351302"+ orientation="1,0"+ id="guide4710"+ inkscape:locked="false" />+ <sodipodi:guide+ position="112.62418,73.773699"+ orientation="0,1"+ id="guide8975"+ inkscape:locked="false" />+ <sodipodi:guide+ position="122.24405,66.315737"+ orientation="0,1"+ id="guide9906"+ inkscape:locked="false" />+ <sodipodi:guide+ position="120.16154,57.421367"+ orientation="0,1"+ id="guide9908"+ inkscape:locked="false" />+ <sodipodi:guide+ position="113.00189,59.859912"+ orientation="1,0"+ id="guide9910"+ inkscape:locked="false" />+ <sodipodi:guide+ position="127.5477,68.84863"+ orientation="0,1"+ id="guide9916"+ inkscape:locked="false" />+ </sodipodi:namedview>+ <metadata+ id="metadata5">+ <rdf:RDF>+ <cc:Work+ rdf:about="">+ <dc:format>image/svg+xml</dc:format>+ <dc:type+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />+ <dc:title></dc:title>+ </cc:Work>+ </rdf:RDF>+ </metadata>+ <g+ inkscape:label="Layer 1"+ inkscape:groupmode="layer"+ id="layer1"+ transform="translate(0,-197)">+ <path+ id="path4657"+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.66666794px;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';text-align:start;letter-spacing:-2.850003px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#2b2b2b;fill-opacity:1;fill-rule:nonzero;stroke:#0f0f0f;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"+ d="m 132.06601,248.47691 v 25.06876 l 2.94142,9.76581 h 7.51427 c 8.89726,0 17.3421,-3.56874 17.3421,-17.59324 0,-14.37636 -8.79666,-17.24133 -18.09606,-17.24133 z m 8.24394,5.98155 h 2.61379 c 4.82563,0 8.39484,1.91012 8.39484,11.25977 0,9.24913 -3.31763,11.61169 -8.29407,11.61169 h -2.71456 z m -27.95075,-5.98155 -7.59592,25.18607 0.003,9.6485 h 5.63273 l 1.75958,-7.48947 h 10.1539 l 1.80971,7.48947 h 8.72195 l -4.27932,-14.21257 0.016,-0.005 -6.21822,-20.61735 z m 4.87567,5.98155 3.66955,15.38148 h -7.33909 z m -41.385799,-5.98137 v 34.83455 h 7.339086 v -12.76769 c 0,-6.43418 -0.653097,-11.05867 -1.055232,-14.22549 l 10.053132,26.99318 h 10.505823 0.003 l -0.001,-2.78846 -0.002,0.008 v -11.04016 l -5.3e-4,-2.46031 h 5.3e-4 v -18.55339 h -7.389133 v 14.07459 c 0,5.57964 0.653473,10.15378 1.105877,12.96873 L 86.103741,248.47709 Z m -66.4031819,0 -2.6137936,34.83455 h 8.0424025 l 0.603581,-13.52165 c 0.201068,-4.8759 0.200974,-9.19902 -0.150895,-14.32626 l 5.730399,24.17889 h 7.791257 l 5.378484,-24.17889 c -0.251338,4.32296 -0.150566,9.35012 0.100767,14.22601 l 0.603062,13.6219 h 8.093046 l -2.61379,-34.83456 H 29.954716 l -4.875673,23.62543 -5.228104,-23.62543 z m 49.0809429,-0.8549 c -9.802067,0 -15.68328,6.88659 -15.68328,18.24695 0,11.61167 5.881213,18.29707 15.68328,18.29707 9.852331,0 15.733924,-6.88647 15.733924,-18.29707 0,-11.61169 -5.881593,-18.24695 -15.733924,-18.24695 z m 0,6.03219 c 4.67483,0 7.087938,3.41802 7.087938,12.21476 0,8.84699 -2.36284,12.26488 -7.087938,12.26488 -4.524031,0 -7.037298,-3.41789 -7.037298,-12.26488 0,-8.79674 2.412733,-12.21476 7.037298,-12.21476 z"+ inkscape:connector-curvature="0" />+ <g+ id="flowRoot4616"+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';text-align:start;letter-spacing:0.38999996px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient4670);stroke-width:1.13385832;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"+ transform="matrix(1.1781329,0,0,1.1781329,271.19496,167.28142)"+ aria-label=" > " />+ <g+ id="flowRoot4650"+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';text-align:start;letter-spacing:0.38999996px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient4678);stroke-width:1.13385832;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"+ transform="matrix(1.1781329,0,0,1.1781329,268.90451,167.19108)"+ aria-label=" > " />+ <g+ id="flowRoot4642"+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.66666794px;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';text-align:start;letter-spacing:-2.850003px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#373737;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+ transform="matrix(1.1781329,0,0,1.1781329,258.78415,166.40418)"+ aria-label="E" />+ <path+ id="path4667"+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.66666794px;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';text-align:start;letter-spacing:-2.850003px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#2b2b2b;fill-opacity:1;fill-rule:nonzero;stroke:#0f0f0f;stroke-width:0.26458333;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;stroke-dashoffset:0"+ d="m 75.849287,210.69008 v 18.55338 h 5.3e-4 l -5.3e-4,2.46032 v 11.04015 l -0.0021,-0.008 -0.0011,2.78846 h 0.0031 10.505818 l 5.727817,-15.37994 7.234184,-19.45464 h -6.879603 l -10.304798,27.04331 c 0.4524,-2.81495 1.105876,-7.38907 1.105876,-12.9687 v -14.07459 z m 25.443413,0 -5.690095,15.30345 c -0.147037,1.99205 -0.249597,4.23297 -0.249597,6.76341 v 12.7677 h 7.339082 v -34.83457 z m 23.97704,5.88046 0.85453,-5.88124 h -21.36346 v 34.83503 h 21.36346 v -5.9315 H 113.0046 v -8.89726 h 10.70687 v -5.78071 H 113.0046 v -8.34432 z M 43.009437,245.52332 40.395553,210.68829 H 29.940016 l -4.875902,23.62549 -5.227765,-23.6255 H 9.431079 L 6.8171912,245.52332 H 14.859916 L 15.46312,232.0015 c 0.201072,-4.8759 0.201072,-9.19886 -0.150801,-14.3261 l 5.730438,24.17843 h 7.791382 l 5.378577,-24.17843 c -0.251343,4.32296 -0.150802,9.34967 0.100494,14.22557 l 0.603205,13.62235 z m 15.502409,-35.68953 c -9.802067,0 -15.68328,6.8866 -15.68328,18.24694 0,11.6117 5.881213,18.29708 15.68328,18.29708 9.852331,0 15.733924,-6.88646 15.733924,-18.29708 0,-11.61168 -5.881593,-18.24694 -15.733924,-18.24694 z m 0,6.0322 c 4.67483,0 7.087938,3.41802 7.087938,12.21475 0,8.847 -2.36284,12.26489 -7.087938,12.26489 -4.524031,0 -7.037298,-3.41789 -7.037298,-12.26489 0,-8.79672 2.412733,-12.21475 7.037298,-12.21475 z"+ inkscape:connector-curvature="0" />+ <path+ style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:57.54236603px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Ultra-Bold';text-align:start;letter-spacing:-1.70070565px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:none;fill-opacity:1;stroke:url(#linearGradient10134);stroke-width:1.00000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"+ d="m 648.5341,87.310007 c -9.7121,4.127281 -19.42418,8.254548 -29.13624,12.381802 0.0453,11.073141 0.0905,22.146271 0.13585,33.219381 10.56235,4.42211 21.12472,8.84421 31.68712,13.2663 M 633.36752,96.140968 c -4.23915,1.801771 -8.47831,3.603539 -12.71747,5.405302 0.0167,3.72718 0.0333,7.45435 0.05,11.18152 m 2.04302,-3.85006 c 0.0397,8.61454 0.0794,17.22908 0.11908,25.84362 14.76048,6.17481 29.52092,12.34963 44.28132,18.52444 M 632.90195,139.0934 c 25.71117,10.75354 51.42244,21.50716 77.13382,32.26084 1.22807,-2.85848 2.45614,-5.71696 3.68421,-8.57544 m -6.90261,9.20272 c 1.03894,0.43471 2.07789,0.86942 3.11683,1.30413 3.95555,-9.19321 7.91108,-18.38642 11.86659,-27.57964 -7.57971,-2.95242 -15.15943,-5.90484 -22.73917,-8.85727 m 2.15824,-2.09073 c -15.67825,-6.11232 -31.35655,-12.22458 -47.03491,-18.33676 12.36993,-5.10497 24.73982,-10.20989 37.10967,-15.31477 m -16.53251,25.20037 c -6.44424,-2.51117 -12.8885,-5.02234 -19.33276,-7.53351 22.69625,-9.36273 45.39238,-18.72545 68.0884,-28.088151 -0.29141,-0.632498 -0.58281,-1.264996 -0.87422,-1.897494 m -69.22838,29.392715 c -0.24128,-0.094 -0.48257,-0.18796 -0.72385,-0.28193 22.68902,-9.36125 45.37812,-18.72258 68.06732,-28.083988 -3.57014,-7.743386 -7.14027,-15.486781 -10.71039,-23.230186 m -23.22126,33.437724 c 10.77256,-4.446834 21.54514,-8.893648 32.31774,-13.340443 -0.57171,-1.240078 -1.14341,-2.480156 -1.71512,-3.720233 m 1.23863,9.141077 c 0.75978,-0.313671 1.51956,-0.627341 2.27934,-0.941012 -4.03208,-8.753337 -8.06417,-17.506664 -12.09627,-26.259983 -0.56183,0.238567 -1.12367,0.477135 -1.68551,0.715703 m 12.5016,22.097459 c 0.0802,-0.03313 0.1605,-0.06626 0.24075,-0.09939 -4.03607,-8.751509 -8.07213,-17.503012 -12.10817,-26.254508 -9.87001,4.191186 -19.74,8.38238 -29.60997,12.573582 m 12.85578,-2.389769 c -22.05754,9.364824 -44.11497,18.729734 -66.17231,28.094728 m -107.274,4.187127 c -0.84131,0.3573 -1.68261,0.7146 -2.52392,1.07191 0.0163,3.90357 0.0326,7.80715 0.049,11.71072 m -3.93823,-4.9983 c 0.0401,8.6513 0.0803,17.3026 0.12039,25.9539 19.88934,8.31229 39.77866,16.6246 59.66795,24.93692 m -4.00821,-7.74504 c 11.9072,4.98365 23.81441,9.96729 35.72164,14.95091 0.91916,-2.13701 1.83832,-4.27401 2.75748,-6.41102 m -16.85174,2.4761 c 4.93606,2.06386 9.87212,4.12772 14.80817,6.19159 3.9554,-9.19637 7.91078,-18.39274 11.86615,-27.5891 -15.16944,-5.91319 -30.33891,-11.82637 -45.50842,-17.73953 m -0.0175,-0.31222 c -9.09947,-3.54582 -18.19894,-7.09162 -27.29839,-10.63741 9.38846,-3.87537 18.77693,-7.75073 28.1654,-11.62607 m -3.45166,4.69425 C 582.61769,102.37262 598.07784,95.991254 613.53799,89.609887 610.07395,82.0933 606.6099,74.576713 603.14586,67.060124 m 13.27545,22.437871 c 0.0236,-0.0097 0.0473,-0.0195 0.0709,-0.02925 -4.03355,-8.751194 -8.0671,-17.502379 -12.10065,-26.253557 -13.39917,5.691875 -26.79835,11.383767 -40.19755,17.075676 m 45.83217,-4.772545 c -1.7876,-3.875989 -3.57519,-7.751978 -5.36278,-11.627969 -9.37578,3.983924 -18.75156,7.967844 -28.12733,11.95176 m 31.84948,-3.206604 c -1.78706,-3.876259 -3.57412,-7.752517 -5.36119,-11.628776 -18.00377,7.649915 -36.00753,15.299838 -54.01126,22.94977 m 26.05047,-5.71335 c -6.70982,2.853463 -13.41964,5.706914 -20.12947,8.560353 m 12.32275,-9.896003 c -17.37721,7.384846 -34.75442,14.769727 -52.13162,22.154645 m -34.54308,3.084334 c 6.67582,4.1e-4 13.35163,8.2e-4 20.02744,0.001 1.3e-4,-1.26754 2.6e-4,-2.535071 4e-4,-3.802607 m -9.40351,8.965447 c 2.67224,-6.9e-4 5.34448,-0.001 8.01672,-0.002 -0.002,-10.545713 -0.004,-21.091419 -0.006,-31.637124 -13.34915,-0.0035 -26.69828,-0.0069 -40.04738,-0.01038 m 38.46844,20.870619 c 8.9e-4,-7.92388 0.002,-15.847764 0.003,-23.77165 -0.53954,3e-6 -1.07909,7e-6 -1.61863,1e-5 m 2.93627,18.341405 c -5.6e-4,-6.302839 -0.001,-12.60568 -0.002,-18.908526 -15.4422,0.0027 -30.88439,0.0054 -46.32658,0.0081 m 45.48451,12.828552 c 9.6e-4,-4.350837 0.002,-8.701673 0.003,-13.052506 -20.90549,-0.0024 -41.81103,-0.0048 -62.71661,-0.0072 m 65.58699,-1.54598 c -17.31771,-0.0021 -34.63544,-0.0042 -51.95318,-0.0062 m 32.6192,6.513975 c -17.50497,0.0034 -35.00998,0.0067 -52.51502,0.0099 2.4e-4,1.894165 4.7e-4,3.78833 7.1e-4,5.682494 m 36.51675,-11.129932 c -13.26226,-0.0036 -26.52453,-0.0071 -39.78682,-0.01065 7.6e-4,10.540165 0.002,21.080346 0.002,31.620542 0.54277,-1.4e-4 1.08555,-2.8e-4 1.62833,-4.3e-4 m 1.41106,-28.98446 c -1.56444,-8.4e-5 -3.12888,-1.68e-4 -4.69332,-2.52e-4 -6.9e-4,10.5424 -0.001,21.084802 -0.002,31.627202 8.20467,-4.4e-4 16.40934,-8.7e-4 24.61401,-0.001 M 426.5625,75.431744 c -4e-5,0.260418 -7e-5,0.520835 -10e-5,0.781252 m 2.74229,1.077147 c 4.6e-4,4.94823 9.2e-4,9.896459 0.001,14.844686 m -5.18108,-9.621664 c -0.002,8.346092 -0.004,16.692179 -0.007,25.038265 10.40075,-5.5e-4 20.80152,-0.001 31.20231,-0.002 M 429.5625,79.743633 c 4.7e-4,7.466811 9.4e-4,14.933626 0.001,22.400447 22.87234,-0.002 45.74463,-0.005 68.61687,-0.007 m -69.06358,44.11522 c 0.002,6.03419 0.003,12.06836 0.005,18.10254 8.29248,0.002 16.58496,0.005 24.87742,0.007 m -23.34184,-3.12116 c 20.17874,0.001 40.35759,0.002 60.53655,0.003 m -23.77935,-1.18297 c 10.52815,-0.001 21.0563,-0.002 31.58447,-0.003 -0.002,-11.50253 -0.005,-23.00505 -0.007,-34.50757 -1.27582,-1.3e-4 -2.55165,-2.7e-4 -3.82747,-4e-4 m -20.02132,38.25354 c 6.77082,-4.3e-4 13.54163,-8.6e-4 20.31244,-0.001 m -7.31097,-4.31121 c 4.10849,-4.6e-4 8.21698,-9.2e-4 12.32546,-0.001 0.003,-11.50258 0.005,-23.00515 0.008,-34.50769 -0.5369,-6e-5 -1.0738,-1.2e-4 -1.6107,-1.8e-4 m 2.79501,32.87573 c 2.9e-4,-1.82248 5.7e-4,-3.64497 8.6e-4,-5.46745 m -3.39149,0.62821 c 6.8e-4,-9.24988 0.001,-18.49975 0.002,-27.74963 -16.2697,1.5e-4 -32.53942,3e-4 -48.80914,4.5e-4 m 17.20351,1.51492 c -12.6271,-0.003 -25.25422,-0.006 -37.88136,-0.008 m 26.95033,1.07868 c -8.46134,5.8e-4 -16.92269,0.001 -25.38406,0.002 m 15.02401,-4.6814 c -6.03953,-0.001 -12.07906,-0.002 -18.1186,-0.004 2e-5,0.9914 4e-5,1.9828 6e-5,2.97421 m 1.86366,1.09184 c -0.98597,0 -1.97193,0 -2.9579,0 -2.1e-4,11.5013 -4.3e-4,23.0026 -6.5e-4,34.50389 12.38231,0 24.76462,-1e-5 37.14693,-3e-5 M 643.21578,45.707549 539.24227,89.88274 m 107.52106,-0.09641 -5.60853,2.382893 m -6.93873,48.252292 85.18969,35.63824"+ transform="matrix(0.26458333,0,0,0.26458333,0,197)"+ id="text10004"+ inkscape:path-effect="#path-effect10153"+ inkscape:original-d="M 708.97266 63.355469 L 619.80469 101.24023 L 619.94922 134.45312 L 709.44922 171.89453 L 721.31055 144.31055 L 652.99609 117.69531 L 721.07227 89.607422 L 708.97266 63.355469 z M 601.36133 63.490234 L 512.19336 101.375 L 512.33984 134.58789 L 601.83789 172.0293 L 613.70117 144.44531 L 545.38477 117.83008 L 613.46094 89.742188 L 601.36133 63.490234 z M 426.85156 73.822266 L 426.85156 105.45117 L 500.01367 105.45117 L 500.01367 73.822266 L 426.85156 73.822266 z M 426.47656 126.69727 L 426.47656 161.20117 L 500.01367 161.20117 L 500.01367 126.69727 L 426.47656 126.69727 z " />+ <path+ inkscape:original-d="m 708.97266,63.355469 -89.16797,37.884761 0.14453,33.21289 89.5,37.44141 11.86133,-27.58398 -68.31446,-26.61524 68.07618,-28.087888 z m -107.61133,0.134765 -89.16797,37.884766 0.14648,33.21289 89.49805,37.44141 11.86328,-27.58399 -68.3164,-26.61523 68.07617,-28.087892 z M 426.85156,73.822266 v 31.628904 h 73.16211 V 73.822266 Z m -0.375,52.875004 v 34.5039 h 73.53711 v -34.5039 z"+ inkscape:path-effect="#path-effect10204;#path-effect10605"+ id="path10202"+ d="m 650.24158,82.877982 c -10.149,3.781746 -20.1801,7.698359 -30.11816,11.721213 3e-5,-0.006 -0.20722,33.232025 -0.20718,33.226045 10.46503,4.53182 20.80539,9.18012 30.97848,13.96782 M 633.98845,89.915578 c -4.37615,1.710896 -8.7325,3.44455 -13.07111,5.19858 1e-5,-4.22e-4 -0.0739,11.184852 -0.0739,11.184432 m 0.37278,-3.80672 c 1e-5,-0.002 -0.17855,25.85224 -0.17854,25.85018 14.60623,6.34072 28.96616,12.91035 42.96367,19.77137 m -31.31836,-14.0728 c 25.19686,11.26895 49.54864,23.28313 72.44078,36.37319 1.425,-2.72943 2.86187,-5.45508 4.31094,-8.17685 m -6.97533,6.68321 c 0.92922,0.52549 1.85609,1.05271 2.78058,1.58169 4.58684,-8.77946 9.29667,-17.51974 14.14,-26.21726 -6.95272,-3.77255 -14.01859,-7.44365 -21.18283,-11.02194 m 1.28755,-0.18554 c -14.8103,-7.42641 -30.04349,-14.4533 -45.56854,-21.15719 12.62298,-4.62803 25.39915,-9.03748 38.37817,-13.169505 m -18.7384,22.832935 c -6.26317,-2.87365 -12.58539,-5.68727 -18.95756,-8.44619 23.14078,-8.47619 46.79403,-16.213872 71.26656,-22.849679 -0.22872,-0.662637 -0.45816,-1.32517 -0.68831,-1.987601 m -71.06157,24.84294 c -2.7e-4,-1.2e-4 -0.71567,-0.30912 -0.71594,-0.30924 23.13582,-8.50099 46.78195,-16.269483 71.24507,-22.942194 -2.81444,-8.108272 -5.73549,-16.201083 -8.75553,-24.2782 m -26.09484,33.263656 c 11.29107,-3.630566 22.7343,-7.053436 34.3625,-10.229723 -0.45247,-1.298072 -0.90767,-2.595747 -1.36557,-3.893024 m -0.61775,6.067313 c 0.81699,-0.225925 1.63488,-0.450636 2.45368,-0.674118 -3.17802,-9.163387 -6.49223,-18.307041 -9.93162,-27.430627 -0.62069,0.179564 -1.24081,0.359777 -1.86037,0.540633 M 724.0723,88.77051 c -3e-5,7e-6 0.2599,-0.07099 0.25987,-0.07098 -3.19166,-9.161723 -6.51908,-18.303673 -9.97123,-27.425513 -10.91455,3.157907 -21.65447,6.515727 -32.24586,10.043417 m 11.85695,-2.48494 c -23.89196,7.592096 -46.99908,16.087727 -69.6127,25.151573 m -0.43513,1.234862 c -0.86165,0.346041 -1.7226,0.692905 -2.58285,1.040573 0,-6.33e-4 -0.0825,11.715567 -0.0825,11.714937 m -1.14486,-4.62264 c 2e-5,-0.003 -0.15936,25.9702 -0.15934,25.96762 19.67736,8.52605 38.90805,17.46409 57.40797,26.96703 m -5.17959,-4.09691 c 11.17635,5.63101 22.10189,11.45969 32.71559,17.51889 1.06572,-2.04091 2.13807,-4.0797 3.21719,-6.11634 m -16.16717,-0.69843 c 4.49732,2.4364 8.94519,4.91012 13.33928,7.42352 4.5879,-8.78027 9.29885,-17.52135 14.14335,-26.21966 -13.9167,-7.55194 -28.28634,-14.69746 -42.99029,-21.50586 m 3.21061,0.9872 c -8.79427,-4.11274 -17.71151,-8.10234 -26.72613,-11.98375 9.58254,-3.52225 19.25313,-6.91934 29.03349,-10.16553 m -6.29154,3.40821 c 16.00756,-5.449894 32.29758,-10.506011 48.96708,-15.053449 -2.73184,-7.869814 -5.56409,-15.725064 -8.48978,-23.565537 m 9.71116,23.601543 c -10e-6,10e-7 0.0757,-0.02052 0.0757,-0.02052 -3.1675,-9.168181 -6.47185,-18.316577 -9.90208,-27.444853 -14.82033,4.260532 -29.31841,8.892287 -43.55961,13.820057 m 47.96444,-1.481223 c -1.46923,-4.051074 -2.96412,-8.098229 -4.4837,-12.141437 -10.36636,2.98215 -20.57513,6.14594 -30.64872,9.46561 m 34.56626,1.430692 c -1.47476,-4.050136 -2.97506,-8.096352 -4.49994,-12.138618 -19.92545,5.740294 -39.26773,12.148987 -58.18539,19.043646 m 27.30172,-7.79642 c -7.20395,2.383141 -14.33903,4.845786 -21.41343,7.378487 m 13.91967,-6.239516 c -18.5565,6.309449 -36.66702,13.130413 -54.47416,20.298837 m -99.88694,-2.65901 c -3.7155,1.714952 -7.42448,3.435794 -11.12812,5.161624 -5e-5,-0.0011 0.51174,11.701562 0.51169,11.700442 m -1.16626,-9.974288 c -2e-5,7e-6 -0.10031,0.04677 -0.10032,0.04677 -4.9e-4,-0.01105 1.45405,33.189175 1.45357,33.178125 14.55592,5.39361 29.0225,10.86451 43.33592,16.46681 m -44.30591,-34.9574 c -1.2e-4,-0.003 0.76588,17.43323 0.76576,17.43045 2.49468,0.92414 4.98679,1.85054 7.47599,2.77947 m -7.79893,-15.73125 c -7e-5,-0.001 0.55089,12.57745 0.55083,12.57602 16.53412,6.12709 32.95515,12.35391 49.1696,18.75966 m -49.998,-25.64703 c -2e-5,-4.2e-4 0.29586,6.72017 0.29584,6.71975 21.69241,8.03149 43.19188,16.23317 64.28749,24.78382 m -55.73364,-22.29341 c 16.28094,6.07855 32.438,12.26676 48.38137,18.64074 M 538.6902,140.8996 c 13.05629,4.98649 26.008,10.06328 38.80781,15.27048 4.95988,2.02065 9.89517,4.0586 14.80138,6.11628 m -42.55708,-18.2693 c 9.31176,3.60951 18.56357,7.2712 27.73805,10.99979 8.32758,3.38914 16.58663,6.82714 24.75588,10.32545 2.01093,-4.82094 4.03838,-9.63839 6.08388,-14.45184 m -27.09202,6.54812 c 6.76764,2.77547 13.48728,5.58447 20.1474,8.43321 3.83317,-9.20422 7.72597,-18.39597 11.68893,-27.57166 -2.66267,-1.06719 -5.33325,-2.12724 -8.0111,-3.18056 m -17.67453,24.80734 c -4e-4,-1.6e-4 0.71086,0.29527 0.71045,0.2951 m 0.14227,0.48986 c 4.4971,1.87026 8.97094,3.75642 13.41807,5.66035 m -10.077,-3.99188 c 3.30432,1.38397 6.59552,2.77682 9.87223,4.17929 3.83114,-9.20405 7.72187,-18.39563 11.68269,-27.57116 -4.68134,-1.87728 -9.38733,-3.73251 -14.11434,-5.56781 m -3.47274,28.46872 c 2.50316,1.05575 4.99856,2.11681 7.4856,3.18351 3.84303,-9.20071 7.74681,-18.38849 11.72182,-27.55979 -12.43487,-4.99967 -25.04372,-9.84155 -37.75845,-14.5654 -3.78675,-1.40608 -7.58297,-2.80119 -11.38756,-4.18633 m 48.53565,21.61219 c -1.7e-4,4e-4 0.39568,-0.91478 0.39551,-0.91439 -12.35213,-4.96947 -24.87863,-9.78356 -37.51252,-14.48141 0,0 0,0 0,0 -0.002,-5.9e-4 -1.49386,-0.55438 -1.49544,-0.55497 m 14.92803,5.28113 c -4.46369,-1.70194 -8.94392,-3.38741 -13.43767,-5.05816 -10.77759,-4.00456 -21.6331,-7.92011 -32.54145,-11.76948 3.60412,-1.59924 7.21392,-3.19123 10.83049,-4.77515 m 3.29049,8.8856 c -4.50077,-1.62071 -9.01238,-3.22836 -13.53303,-4.82458 10.46006,-4.6382 20.96883,-9.21505 31.55249,-13.710221 6.70151,-2.847434 13.43404,-5.663734 20.20532,-8.439763 m -42.45719,26.470664 c -2.78088,-0.99541 -5.56571,-1.98604 -8.35408,-2.97227 3.53015,-1.56453 7.06581,-3.12201 10.60798,-4.67164 m -6.28221,1.12087 c 8.71898,-3.84186 17.47467,-7.63886 26.28214,-11.379291 6.23906,-2.650669 12.50495,-5.274355 18.80392,-7.863686 m -30.08413,13.695717 c 1.69004,-0.7302 3.38175,-1.45849 5.07524,-2.18477 m -3.70952,0.27076 c 3.29638,-1.42091 6.59931,-2.83445 9.90956,-4.240003 12.08898,-5.135115 24.2791,-10.168894 36.61561,-15.047723 -3.81907,-8.196246 -7.69879,-16.377835 -11.63022,-24.544496 m 7.66355,15.993468 c -2.66832,-5.660129 -5.36415,-11.313206 -8.08454,-16.959141 -6.13435,2.520141 -12.22725,5.075193 -18.28428,7.658738 m 21.8501,0.239974 c -1.19988,-2.513918 -2.40488,-5.026433 -3.61474,-7.537535 -5.45054,2.240469 -10.86844,4.508482 -16.25759,6.799535 m 12.33589,-6.475006 c -6.71417,2.780884 -13.37956,5.60218 -20.00355,8.455418 m 5.22362,-1.424866 c -2.09601,0.894819 -4.18772,1.793033 -6.27538,2.694375 -21.33032,9.189731 -42.27864,18.699376 -63.05589,28.36997 m 73.14903,-32.387689 c -3.37392,1.430271 -6.73645,2.869706 -10.08856,4.317204 -14.05547,6.056609 -27.94537,12.252172 -41.73006,18.541186 m 34.13573,-15.100677 c -13.55115,5.910939 -26.95746,11.944435 -40.27371,18.059191 M 567.455,71.414256 c -10.01024,4.371971 -19.94169,8.810224 -29.81644,13.298103 m 19.59924,-7.3737 c -15.52737,6.889358 -30.88337,13.926195 -46.1519,21.047284 -10e-5,-0.0022 0.70264,16.025187 0.70254,16.022947 m 0.87651,-18.197631 c -1.9e-4,8.6e-5 -0.35812,0.16672 -0.3583,0.166806 m 1.08448,0.822714 c -0.004,0.0019 -1.69243,0.788488 -1.69657,0.790417 -1.3e-4,-0.0029 0.84103,19.229244 0.8409,19.226364 m 0.27212,-20.309867 c -7.4e-4,3.43e-4 -0.71392,0.332461 -0.71466,0.332804 -4e-4,-0.0093 1.44469,33.189763 1.44429,33.180483 11.28017,4.18412 22.50656,8.41567 33.64949,12.71984 m -35.32215,-28.47471 c -1.1e-4,-0.002 0.69794,15.953 0.69784,15.95059 17.67814,6.55284 35.2254,13.22036 52.52747,20.09941 m -13.02727,-5.4797 c 6.64819,2.58659 13.26492,5.20045 19.84385,7.84699 m -14.54026,-5.47963 c 3.37527,1.32489 6.74192,2.65722 10.09913,3.9977 m -5.39559,-1.55982 c 5.16536,2.04368 10.30944,4.10566 15.42921,6.18851 8.03258,3.27252 16.00065,6.59042 23.88508,9.96399 3.57375,-8.58209 7.19933,-17.15333 10.88525,-25.71084 m -48.34658,8.62447 c 4.53399,1.79806 9.0515,3.61046 13.55048,5.43895 8.57906,3.49155 17.0857,7.03497 25.49668,10.6428 1.91851,-4.59483 3.85215,-9.18644 5.80223,-13.77439 m -41.52536,0.52971 c 3.42676,1.36777 6.84367,2.74402 10.24984,4.1295 8.35495,3.40329 16.64023,6.85569 24.83429,10.36877 3.83709,-9.20147 7.73433,-18.39024 11.70222,-27.56272 -5.91943,-2.37828 -11.87854,-4.72102 -17.86996,-7.03252 m 13.95351,4.36339 c -0.002,-8.8e-4 -1.79934,-0.7141 -1.80156,-0.71498 m 0.25261,1.27066 c -10.41553,-4.13 -20.94603,-8.15491 -31.55191,-12.09788 -10.5318,-3.91299 -21.13804,-7.741 -31.79532,-11.50531 -0.0133,0.006 3.63504,-1.60824 3.62177,-1.60235 m 48.0644,20.84878 c -4.71741,-1.82326 -9.45512,-3.62703 -14.20953,-5.41341 m 2.40101,-0.2426 c -2.69367,-1.01619 -5.39282,-2.02666 -8.09682,-3.03181 -10.38933,-3.85968 -20.85046,-7.63674 -31.36094,-11.3516 -0.0155,0.007 3.94411,-1.74382 3.92864,-1.73696 m 20.404,11.06404 c -7.93015,-2.91003 -15.89918,-5.77486 -23.89719,-8.60348 5.07413,-2.2482 10.15969,-4.48179 15.25969,-6.69847 m -13.61649,6.6743 c -0.009,-0.003 -3.05451,-1.07992 -3.06321,-1.08301 10.59224,-4.6966 21.23429,-9.33057 31.95333,-13.88081 7.21165,-3.062608 14.45934,-6.08915 21.75269,-9.068245 m -8.83881,3.866756 c 7.8818,-3.27547 15.81275,-6.500433 23.80517,-9.66031 -0.90933,-1.951335 -1.82211,-3.901839 -2.73821,-5.851509 m 2.26326,3.123733 c -3.697,-7.91409 -7.45009,-15.814457 -11.25119,-23.700854 -8.5632,3.512555 -17.0451,7.093405 -25.46084,10.725134 -4.85982,2.092681 -9.69974,4.201958 -14.52227,6.325955 M 445.3614,104.95449 c 9.76321,-0.4568 1.68049,-0.0787 11.4437,-0.53547 m -10.32307,0.17019 c 17.33768,-0.81149 34.68495,-1.62843 52.0441,-2.35925 0,2e-5 -0.46244,-11.003335 -0.46244,-11.003316 m -45.86213,13.509466 c 10.91926,-0.51038 21.84211,-1.01894 32.76916,-1.5028 m -10.72125,-0.82512 c 8.068,-0.3673 16.13869,-0.72521 24.21227,-1.06452 0.002,0.0444 -1.33216,-31.644111 -1.33029,-31.599679 -6.59025,0.277249 -13.17067,0.566548 -19.74138,0.862977 m 2.25651,0.535676 c -7.30576,0.326528 -36.16329,1.682218 -43.46906,2.008746 m 35.77617,-0.870836 c -10.80151,0.493925 -31.83544,1.492911 -42.63695,1.986836 m 3.35975,-1.504342 c 0.39499,-0.01836 -9.27129,0.428216 -8.87629,0.409857 -0.001,-0.02207 1.44614,31.619645 1.44513,31.597575 0.10366,-0.005 5.20914,-0.23889 5.31279,-0.24362 m -0.2042,-30.76454 c 0.20369,-0.0094 -6.92249,0.318369 -6.71879,0.308951 -9.8e-4,-0.02146 1.44069,31.616219 1.43972,31.594769 0.1859,-0.008 6.89529,-0.31723 7.08119,-0.32573 m -6.69291,-6.800598 c 0,-4e-6 0.3181,6.962618 0.3181,6.962618 0.24448,-0.0112 7.70616,-0.35518 7.95064,-0.36637 m -8.35402,-1.37166 c 0,0 0.0827,1.80758 0.0827,1.80758 3.0696,-0.14043 22.32683,-1.04088 25.39644,-1.18132 m 20.54416,-1.203 c 8.98418,-0.41193 17.97134,-0.81355 26.9618,-1.19215 0.002,0.0378 -1.33217,-31.639357 -1.33058,-31.601546 -10.17056,0.427545 -20.31809,0.883734 -30.44304,1.350483 m 30.68664,-0.72718 c -20.40193,0.858688 -40.71263,1.833171 -60.93582,2.777436 m 28.41997,-1.684025 c -4.1856,0.193669 -19.90118,0.931629 -24.08678,1.125298 m 22.17602,-1.068122 c -22.28414,1.034703 -17.10212,0.801742 -39.38626,1.836445 -1.4e-4,-0.0031 0.53538,11.761564 0.53524,11.758433 m 32.28599,-14.126315 c -95.89965,4.469127 62.94442,-2.937182 -32.95522,1.531945 -1e-4,-0.0021 0.45257,9.905323 0.45248,9.903238 M 448.9164,72.551881 c 11.72288,-0.549159 -36.94844,1.722573 -25.22557,1.173414 -0.001,-0.0234 1.44333,31.619995 1.44226,31.596595 6.6714,-0.30451 28.44286,-1.3283 35.11426,-1.63281 m -34.01902,1.55845 c 13.52181,-0.61825 30.67232,-1.43431 44.19413,-2.05256 m -9.57078,1.2155 c 12.84618,-0.5986 25.69748,-1.18633 38.55486,-1.72596 0.003,0.069 -1.20208,-28.704378 -1.19919,-28.635343 m -20.193,84.780593 c -0.21015,0.01 8.44865,-0.38035 8.2385,-0.37079 m -8.41912,0.49633 c 7.79028,-0.35489 15.56278,-0.70005 23.3177,-1.02705 0.007,0.17784 -1.4534,-34.65264 -1.44594,-34.47479 -7.23278,0.30417 -14.47002,0.62366 -21.7119,0.95178 m 9.43891,34.49345 c 4.35631,-0.19375 8.70702,-0.38336 13.05218,-0.56734 0.003,0.066 -1.45708,-34.542 -1.4543,-34.47599 -0.0565,0.002 -5.57864,0.23801 -5.63515,0.24039 m 7.67428,28.58029 c 0,0 -0.40982,-9.7569 -0.40982,-9.7569 m 0.84061,6.75643 c 0.001,0.0346 -1.11214,-26.54197 -1.1107,-26.50738 -17.97917,0.75429 -35.98538,1.60458 -54.02121,2.44789 m 55.01929,23.80495 c 0.007,0.15617 -0.85568,-20.45303 -0.84915,-20.29687 m 0.21924,1.51522 c 3.1e-4,0.008 -0.35042,-8.37985 -0.3501,-8.37232 -10.5658,0.44215 -21.14036,0.91746 -31.72418,1.40505 m 9.07703,1.51937 c -16.95737,0.77063 -35.92067,1.68342 -52.87805,2.45405 -0.001,-0.0286 1.56923,34.49986 1.56793,34.4713 0.058,-0.003 4.17146,-0.18972 4.22949,-0.19234 m -4.98992,-19.88834 c -3.8e-4,-0.008 0.87723,19.28496 0.87684,19.27656 18.6697,-0.84824 35.2157,-1.64809 53.8854,-2.49633 m 11.79856,0.1906 c -0.15774,0.007 8.99135,-0.38633 8.83362,-0.37944 -10e-6,-8e-5 -1.43579,-34.1165 -1.43579,-34.11659 m -1.69603,33.35019 c -0.0262,0.001 3.81643,-0.16203 3.79027,-0.16091 0.0157,0.37447 -1.4585,-34.85081 -1.44283,-34.47634 -0.0214,8.9e-4 -3.5105,0.14826 -3.53186,0.14915 m 4.61922,19.10912 c 0.004,0.0858 -0.78458,-18.76752 -0.78099,-18.68177 -24.53892,1.02893 -49.13037,2.23745 -73.78111,3.36364 m 73.39687,-3.66582 c 0,0 -0.0473,-1.127 -0.0473,-1.127 -8.93557,0.37427 -17.87744,0.77214 -26.82593,1.18099 m 15.15007,0.12688 c -0.22906,0.0101 -9.6177,0.43036 -9.84676,0.44045 m 1.52061,0.34501 c -16.24181,0.73155 -38.96197,1.82062 -55.20378,2.55217 -0.001,-0.0237 1.47431,32.45127 1.47323,32.42759 m 40.34148,-35.16514 c -25.12414,1.16377 -16.34142,0.76449 -41.46556,1.92826 -7.8e-4,-0.017 1.2093,26.53281 1.20852,26.51577 m 24.69571,-28.18027 c 9.53772,-0.44541 -35.81094,1.66128 -26.27322,1.21588 -0.001,-0.0299 1.57115,34.49543 1.56979,34.46556 0.0762,-0.003 4.73457,-0.2163 4.81073,-0.21977 m -2.79221,-33.16409 c 0.0241,-0.001 -2.59964,0.11865 -2.57549,0.11755 -4e-4,-0.009 0.8109,17.75591 0.8105,17.74721 m -1.05404,-3.95994 c -4.1e-4,-0.009 0.89319,19.58425 0.89278,19.57525 0.1,-0.005 5.38575,-0.24536 5.48572,-0.24989 m -0.33745,0.29462 c 18.27801,-0.83737 27.86561,-1.30104 46.14361,-2.13842 m -30.04463,0.96977 c 1.40741,-0.0658 6.09013,-0.28494 7.49755,-0.35072 m -6.93473,1.30994 c 17.84317,-0.83318 35.5931,-1.67079 53.25223,-2.41325 0.001,0.0251 -0.89222,-21.27408 -0.89117,-21.24899 M 645.91022,40.260018 C 621.98143,48.971525 598.82716,58.393162 576.151,68.183886 563.38251,73.685152 550.75083,79.301063 538.21074,84.99752 m 110.06472,-0.559322 c -1.94975,0.731393 -3.89521,1.467714 -5.83656,2.208758 m -8.17032,48.075284 c 27.78415,12.47907 54.52224,25.87525 79.38684,40.63376"+ style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:57.54236603px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Ultra-Bold';text-align:start;letter-spacing:-1.70070565px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient10134);stroke-width:2.00000003;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"+ transform="matrix(0.26458333,0,0,0.26458333,0,197)"+ inkscape:connector-curvature="0" />+ <text+ xml:space="preserve"+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.11666656px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans';text-align:start;letter-spacing:0px;word-spacing:-0.26458333px;writing-mode:lr-tb;text-anchor:start;fill:#b2b2b2;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"+ x="247.48691"+ y="151.86511"+ id="text4786"+ transform="rotate(23.112766)"><tspan+ sodipodi:role="line"+ id="tspan4784"+ x="247.48691"+ y="151.86511"+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.11666656px;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans';word-spacing:-0.26458333px;stroke-width:0.26458332px;fill:#b2b2b2;fill-opacity:1;">(C) DAVID LETTIER</tspan></text>+ </g>+</svg>