packages feed

Gifcurry-2.2.0.0: src/gui/Main.hs

{-
  Gifcurry
  (C) 2016 David Lettier
  lettier.com
-}

{-# LANGUAGE OverloadedStrings #-}

import System.Directory
import System.Process
import System.FilePath
import System.Info
import System.IO.Temp
import Text.Read
import Data.Int
import Data.Maybe
import Data.Either
import Data.Char
import Data.Text
import Data.List
import Control.Monad
import Control.Concurrent
import Data.GI.Base
import GI.GObject
import qualified GI.Gtk

import Paths_Gifcurry
import qualified Gifcurry (
      gif
    , GifParams(..)
    , defaultGifParams
    , gifParamsValid
    , getVideoDurationInSeconds
  )
import qualified GtkMainSyncAsync (gtkMainSync, gtkMainAsync)

main :: IO ()
main = do
  _ <- GI.Gtk.init Nothing

  builder <- buildBuilder

  window                  <- builderGetObject GI.Gtk.Window            builder "gifcurry-window"
  startTimeEntry          <- builderGetObject GI.Gtk.Entry             builder "start-time-text-entry"
  durationTimeEntry       <- builderGetObject GI.Gtk.Entry             builder "duration-text-entry"
  widthEntry              <- builderGetObject GI.Gtk.Entry             builder "width-text-entry"
  qualityEntry            <- builderGetObject GI.Gtk.Entry             builder "quality-text-entry"
  topTextEntry            <- builderGetObject GI.Gtk.Entry             builder "top-text-text-entry"
  bottomTextEntry         <- builderGetObject GI.Gtk.Entry             builder "bottom-text-text-entry"
  outputFileNameEntry     <- builderGetObject GI.Gtk.Entry             builder "output-file-name-text-entry"
  statusEntry             <- builderGetObject GI.Gtk.Entry             builder "status-text-entry"
  inputFileButton         <- builderGetObject GI.Gtk.FileChooserButton builder "input-file-button"
  outputFilePathButton    <- builderGetObject GI.Gtk.FileChooserButton builder "output-file-path-button"
  fontChooserButton       <- builderGetObject GI.Gtk.FontButton        builder "font-chooser-button"
  createButton            <- builderGetObject GI.Gtk.Button            builder "create-button"
  openButton              <- builderGetObject GI.Gtk.Button            builder "open-button"
  giphyButton             <- builderGetObject GI.Gtk.Button            builder "giphy-link-button"
  imgurButton             <- builderGetObject GI.Gtk.Button            builder "imgur-link-button"
  firstFrameImage         <- builderGetObject GI.Gtk.Image             builder "first-frame-image"
  lastFrameImage          <- builderGetObject GI.Gtk.Image             builder "last-frame-image"
  longGifGtkMessageDialog <- builderGetObject GI.Gtk.MessageDialog     builder "long-gif-message-dialog"
  yesGtkButton            <- builderGetObject GI.Gtk.Button            builder "yes-button"
  noGtkButton             <- builderGetObject GI.Gtk.Button            builder "no-button"
  aboutButton             <- builderGetObject GI.Gtk.Button            builder "about-button"
  aboutDialog             <- builderGetObject GI.Gtk.AboutDialog       builder "about-dialog"

  -- Bug in Glade does not allow setting the link button label.
  GI.Gtk.buttonSetLabel giphyButton "Giphy"
  GI.Gtk.buttonSetLabel imgurButton "Imgur"

  GI.Gtk.entrySetText qualityEntry "100"
  GI.Gtk.entrySetText widthEntry   "500"

  -- Glade does not allow us to use the response ID nicknames so we setup them up programmatically here.
  GI.Gtk.dialogAddActionWidget longGifGtkMessageDialog yesGtkButton (enumToInt32 GI.Gtk.ResponseTypeYes)
  GI.Gtk.dialogAddActionWidget longGifGtkMessageDialog noGtkButton  (enumToInt32 GI.Gtk.ResponseTypeNo)

  _ <- GI.Gtk.onFileChooserButtonFileSet inputFileButton $ do
    setStatusEntryReady statusEntry
    GI.Gtk.imageSetFromIconName firstFrameImage
      (Just $ pack blankPreviewIcon) (enumToInt32 GI.Gtk.IconSizeButton)
    GI.Gtk.imageSetFromIconName lastFrameImage
      (Just $ pack blankPreviewIcon) (enumToInt32 GI.Gtk.IconSizeButton)
    inputFilePathName <- inputFileButtonGetText inputFileButton
    maybeDuration <- Gifcurry.getVideoDurationInSeconds (
        Gifcurry.defaultGifParams { Gifcurry.inputFile = inputFilePathName }
      )
    let duration = fromMaybe 0.5 maybeDuration - 0.5
    let durationText = if duration == 0.0 then "" else pack (show duration)
    let startText = if duration == 0.0 then "" else "0.0"
    _ <- GI.Gtk.entrySetText startTimeEntry startText
    _ <- GI.Gtk.entrySetText durationTimeEntry durationText
    unless (Data.Text.null startText || Data.Text.null durationText) (
        makeFirstFramePreview inputFileButton startTimeEntry durationTimeEntry firstFrameImage lastFrameImage
      )

  _ <- GI.Gtk.onEditableChanged startTimeEntry $
    makeFirstFramePreview inputFileButton startTimeEntry durationTimeEntry firstFrameImage lastFrameImage

  _ <- GI.Gtk.onEditableChanged durationTimeEntry $
    makeLastFramePreview inputFileButton startTimeEntry durationTimeEntry lastFrameImage

  _ <- GI.Gtk.onWidgetButtonReleaseEvent createButton $ \ _ -> do
    inputFilePathName <- inputFileButtonGetText inputFileButton
    startTime         <- entryGetFloat startTimeEntry (-1.0)
    durationTime      <- entryGetFloat durationTimeEntry (-1.0)
    widthSize'        <- entryGetFloat widthEntry 0.0
    let widthSize     = truncate widthSize'
    qualityPercent    <- entryGetFloat qualityEntry (-1.0)
    fontChoice        <- GI.Gtk.fontButtonGetFontName fontChooserButton
    topText           <- GI.Gtk.entryGetText topTextEntry
    bottomText        <- GI.Gtk.entryGetText bottomTextEntry
    (_, outputFilePathName) <- assembleOutputFilePathName outputFilePathButton outputFileNameEntry
    let params = Gifcurry.defaultGifParams {
        Gifcurry.inputFile      = inputFilePathName
      , Gifcurry.outputFile     = outputFilePathName
      , Gifcurry.startTime      = startTime
      , Gifcurry.durationTime   = durationTime
      , Gifcurry.widthSize      = widthSize
      , Gifcurry.qualityPercent = qualityPercent
      , Gifcurry.fontChoice     = unpack fontChoice
      , Gifcurry.topText        = unpack topText
      , Gifcurry.bottomText     = unpack bottomText
    }
    paramsValid <- Gifcurry.gifParamsValid params
    setStatusEntryReady statusEntry
    if paramsValid
      then do
        longGifGtkMessageDialogResponse <- if durationTime >= durationTimeWarningLevel
                                              then GI.Gtk.dialogRun longGifGtkMessageDialog
                                              else return (enumToInt32 GI.Gtk.ResponseTypeYes)
        when (longGifGtkMessageDialogResponse == enumToInt32 GI.Gtk.ResponseTypeYes) $
          void $ forkOS $ do
            GtkMainSyncAsync.gtkMainAsync $ GI.Gtk.entrySetText statusEntry "One GIF coming up!"
            success <- (ioSuccess . Gifcurry.gif) params
            if not success
              then GtkMainSyncAsync.gtkMainAsync $ GI.Gtk.entrySetText statusEntry "Did not work. Check your settings."
              else do
                _ <- forkOS $ GtkMainSyncAsync.gtkMainAsync $ openGifCommand outputFilePathName
                GtkMainSyncAsync.gtkMainAsync $ setStatusEntryReady statusEntry
      else GI.Gtk.entrySetText statusEntry "Settings are wrong."
    return True

  _ <- GI.Gtk.onWidgetButtonReleaseEvent openButton $ \ _ -> do
    (_, outputFilePathName) <- assembleOutputFilePathName outputFilePathButton outputFileNameEntry
    fileExists <- doesFileExist outputFilePathName
    if fileExists
      then void $ forkIO $ GtkMainSyncAsync.gtkMainAsync $ openGifCommand outputFilePathName
      else GI.Gtk.entrySetText statusEntry "GIF does not exist. Check your settings."
    return True

  _ <- GI.Gtk.onWidgetButtonReleaseEvent aboutButton $ \ _ -> GI.Gtk.dialogRun aboutDialog >> return True

  _ <- GI.Gtk.onDialogResponse longGifGtkMessageDialog (\ _ -> GI.Gtk.widgetHide longGifGtkMessageDialog)
  _ <- GI.Gtk.onDialogResponse aboutDialog             (\ _ -> GI.Gtk.widgetHide aboutDialog)

  _ <- GI.Gtk.onWidgetDestroy window GI.Gtk.mainQuit

  GI.Gtk.widgetShowAll window
  GI.Gtk.main

buildBuilder :: IO GI.Gtk.Builder
buildBuilder = do
  gladeFile <- getDataFileName "data/gui.glade"
  GI.Gtk.builderNewFromFile (pack gladeFile)

builderGetObject ::
  (GI.GObject.GObject b, GI.Gtk.IsBuilder a) =>
  (Data.GI.Base.ManagedPtr b -> b) ->
  a ->
  Prelude.String ->
  IO b
builderGetObject objectTypeClass builder objectId =
  fromJust <$> GI.Gtk.builderGetObject builder (pack objectId) >>=
  GI.Gtk.unsafeCastTo objectTypeClass

inputFileButtonGetText :: GI.Gtk.FileChooserButton -> IO String
inputFileButtonGetText inputFileButton = do
  inputFilePathName <- fileChooserButtonGetText inputFileButton
  fileExist <- doesFileExist inputFilePathName
  if fileExist then return inputFilePathName else return ""

durationTimeWarningLevel :: Float
durationTimeWarningLevel = 10.0

entryGetFloat :: GI.Gtk.Entry -> Float -> IO Float
entryGetFloat e nothing = fmap (\ t -> fromMaybe nothing (readMaybe (unpack t) :: Maybe Float)) (GI.Gtk.entryGetText e)

assembleOutputFilePathName :: GI.Gtk.FileChooserButton -> GI.Gtk.Entry -> IO (String, String)
assembleOutputFilePathName outputFilePathButton outputFileNameEntry = do
  outputFilePath <- fileChooserButtonGetText outputFilePathButton
  outputFileName <- unpack . Data.Text.strip <$> GI.Gtk.entryGetText outputFileNameEntry
  let outputGifFileName = outputFileName ++ ".gif"
  let outputFilePathName =
        if (not . Data.List.null) outputFilePath
          then outputFilePath ++ [System.FilePath.pathSeparator] ++ outputGifFileName
          else outputGifFileName
  return (outputGifFileName, outputFilePathName)

fileChooserButtonGetText :: GI.Gtk.FileChooserButton -> IO String
fileChooserButtonGetText =
  fmap (Data.Text.unpack . Data.Text.strip . Data.Text.pack . fromMaybe "") . GI.Gtk.fileChooserGetFilename

openGifCommand :: String -> IO ()
openGifCommand outputFilePathName = do
  fileExists <- doesFileExist outputFilePathName
  when fileExists $ void (spawnCommand (command ++ "\"" ++ outputFilePathName ++ "\""))
  where
    command :: String
    command = if "linux" `Data.List.isInfixOf` fmap Data.Char.toLower System.Info.os
                then "xdg-open "
                else "open "

blankPreviewIcon :: String
blankPreviewIcon = "gtk-discard"

resetImage :: GI.Gtk.Image -> IO ()
resetImage image =
  GI.Gtk.imageSetFromIconName
    image
    (Just $ pack blankPreviewIcon)
    (enumToInt32 GI.Gtk.IconSizeButton)

makeGifPreview :: String -> String -> Float -> String -> IO (Either IOError String)
makeGifPreview inputFile outputFile startTime bottomText = Gifcurry.gif $ Gifcurry.defaultGifParams {
      Gifcurry.inputFile      = inputFile
    , Gifcurry.outputFile     = outputFile
    , Gifcurry.startTime      = startTime
    , Gifcurry.durationTime   = 0.001
    , Gifcurry.widthSize      = 200
    , Gifcurry.qualityPercent = 50.0
    , Gifcurry.bottomText     = bottomText
  }

ioSuccess :: IO (Either IOError String) -> IO Bool
ioSuccess = fmap isRight

framePreviewDirectoryName :: String
framePreviewDirectoryName = "gifcurry-frame-previews"

makeLastFramePreview :: GI.Gtk.FileChooserButton -> GI.Gtk.Entry -> GI.Gtk.Entry -> GI.Gtk.Image -> IO ()
makeLastFramePreview inputFileButton startTimeEntry durationTimeEntry lastFrameImage =
  void $ forkIO $
    withTempDirectory "." framePreviewDirectoryName $ \ tmpdir -> do
      inputFilePathName           <- inputFileButtonGetText inputFileButton
      startTimeText               <- GI.Gtk.entryGetText startTimeEntry
      durationTimeText            <- GI.Gtk.entryGetText durationTimeEntry
      startTime'                  <- entryGetFloat startTimeEntry (-1.0)
      durationTime                <- entryGetFloat durationTimeEntry 0.001
      let startTimeTextValid      = (not . Data.List.null) (unpack startTimeText)
      let durationTimeTextValid   = (not . Data.List.null) (unpack durationTimeText)
      let startTime               = startTime' + durationTime
      let outputFilePathName      = tmpdir ++ "/gifcurry-last-frame-preview.gif"
      let doSet                   = startTimeTextValid &&
                                    durationTimeTextValid &&
                                    not (Data.List.null inputFilePathName) &&
                                    (startTime > 0.0)
      _ <- setOrResetFramePrevew doSet inputFilePathName outputFilePathName startTime lastFrameImage " LAST  FRAME "
      return()

makeFirstFramePreview :: GI.Gtk.FileChooserButton -> GI.Gtk.Entry -> GI.Gtk.Entry -> GI.Gtk.Image -> GI.Gtk.Image -> IO ()
makeFirstFramePreview inputFileButton startTimeEntry durationTimeEntry firstFrameImage lastFrameImage =
  void $ forkIO $ do
    withTempDirectory "." framePreviewDirectoryName $ \ tmpDir -> do
      inputFilePathName       <- inputFileButtonGetText inputFileButton
      startTime               <- entryGetFloat startTimeEntry (-1.0)
      let outputFilePathName  = tmpDir ++ "/gifcurry-first-frame-preview.gif"
      let doSet               = not (Data.List.null inputFilePathName) && (startTime >= 0.0)
      _ <- setOrResetFramePrevew doSet inputFilePathName outputFilePathName startTime firstFrameImage " FIRST FRAME "
      return ()
    makeLastFramePreview inputFileButton startTimeEntry durationTimeEntry lastFrameImage

setOrResetFramePrevew :: Bool -> String -> String -> Float -> GI.Gtk.Image -> String -> IO ()
setOrResetFramePrevew False _ _ _ image _ = GtkMainSyncAsync.gtkMainAsync $ resetImage image
setOrResetFramePrevew True inputFilePathName outputFilePathName time image overlay = do
  success <- ioSuccess (makeGifPreview inputFilePathName outputFilePathName time overlay)
  _ <- updatePreviewFrame outputFilePathName image success
  return()

updatePreviewFrame :: String -> GI.Gtk.Image -> Bool -> IO ()
updatePreviewFrame filePathName image True  = GtkMainSyncAsync.gtkMainSync (GI.Gtk.imageSetFromFile image (Just filePathName))
updatePreviewFrame _            image False = GtkMainSyncAsync.gtkMainSync (resetImage image)

setStatusEntryReady :: GI.Gtk.Entry -> IO ()
setStatusEntryReady = flip GI.Gtk.entrySetText "Ready."

enumToInt32 :: (Enum a, Ord a) => a -> Int32
enumToInt32 enum = fromIntegral (fromEnum enum) :: Int32