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