diff --git a/app/EpubParser.hs b/app/EpubParser.hs index 903de7d..83b0acd 100644 --- a/app/EpubParser.hs +++ b/app/EpubParser.hs @@ -17,7 +17,7 @@ import Codec.Epub.Parse (getManifest, getMetadata, getSpine) import Control.Applicative ((<|>)) import Control.Monad (guard) import Control.Monad.Except (runExceptT) -import Control.Monad.Reader (MonadReader (ask), liftIO) +import Control.Monad.Reader (MonadReader (ask), liftIO, runReaderT) import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy as B import Data.List (find) @@ -26,8 +26,35 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as TE import System.FilePath (normalise, takeDirectory, ()) import Text.HTML.Scalpel (Scraper, ScraperT, anySelector, attr, chroot, chroots, html, innerHTML, scrapeStringLike, text) -import Types (Chapter (..), EpubAction, EpubEnv (..), InternalPath(..), ChapterIndex(..)) +import Types (Chapter (..), EpubAction, EpubEnv (..), InternalPath(..), ChapterIndex(..), BookInfo(..), EpubType(..)) import Navigation (navFinder, lndmkScraper) +import Codec.Epub.Data.Metadata (metaTitles, titleText, titleType, Creator (creatorText), Metadata (metaCreators)) + +instance BookInfo EpubEnv where + getTitle env = + let titles = metaTitles (eMetadata env) + in case find (\t -> titleType t == Just "main") titles of + Just t -> T.pack (titleText t) + Nothing -> if null titles then "Unknown" else T.pack (titleText (head titles)) + + getAuthors env = map (T.pack . creatorText) (metaCreators (eMetadata env)) + + getLandmarks env = + let xmlStr = opfXml env + maybeNavPath = scrapeStringLike xmlStr navFinder + in case maybeNavPath of + Nothing -> [] + Just path -> + let full = normalise (baseDir env path) + in case findEntryByPath full (archive env) of + Nothing -> [] + Just entry -> + let raw = TE.decodeUtf8 . B.toStrict $ fromEntry entry + in fromMaybe [] (scrapeStringLike raw (lndmkScraper [EpubType "toc", EpubType "landmarks", EpubType "bodymatter"])) + + getBookPath = bookPath + getSpine = runReaderT resolveSpine + loadChapter env path idx = runReaderT (getChapter path idx) env openEpub :: FilePath -> IO (Either String EpubEnv) openEpub path = do @@ -79,7 +106,7 @@ resolveSpine = do env <- ask let xmlStr = opfXml env - spineResult <- liftIO $ runExceptT $ getSpine xmlStr + spineResult <- liftIO $ runExceptT $ Codec.Epub.Parse.getSpine xmlStr let (DMan.Manifest items) = eManifest env case spineResult of diff --git a/app/Types.hs b/app/Types.hs index 950db38..f9d7942 100644 --- a/app/Types.hs +++ b/app/Types.hs @@ -11,6 +11,7 @@ module Types ChapterIndex(..), InternalPath(..), EpubType(..), + BookInfo(..) ) where @@ -22,6 +23,15 @@ import Control.Concurrent.Async (Async) import Control.Monad.Reader (ReaderT) import Data.String (IsString) +-- Unified interface for EPub 2 and EPub 3 formats, allows generic programming of both without having to repeat boilerplate +class BookInfo a where + getTitle :: a -> T.Text + getAuthors :: a -> [T.Text] + getLandmarks :: a -> [NavItem] + getBookPath :: a -> FilePath + getSpine :: a -> IO [(InternalPath, T.Text)] + loadChapter :: a -> (InternalPath, T.Text) -> ChapterIndex -> IO Chapter + newtype ChapterIndex = ChapterIndex Int deriving (Show, Eq, Ord, Num) diff --git a/app/UI.hs b/app/UI.hs index e3b0512..17fbd25 100644 --- a/app/UI.hs +++ b/app/UI.hs @@ -22,7 +22,7 @@ import qualified GI.WebKit as WebKit import Persistence (loadLastRead, saveLastRead) import State (initialState, prepareView) import Text.HTML.Scalpel (Scraper, attr, chroots, text) -import Types (AppState (..), ChapterView (..), UserAction (..), ChapterIndex(..)) +import Types (AppState (..), ChapterView (..), UserAction (..), ChapterIndex(..), BookInfo (..)) updateUI :: Gtk.ApplicationWindow -> WebKit.WebView -> ChapterView -> ChapterIndex -> Int -> IO () updateUI window webView view (ChapterIndex currentIdx) totalCount = do @@ -62,23 +62,22 @@ updateZoom stateTVar webView level = do atomically $ modifyTVar stateTVar $ \s -> s {zoomlvl = level} WebKit.webViewSetZoomLevel webView level -initAppServices :: EpubEnv -> TVar AppState -> (UserAction -> IO ()) -> IO () +initAppServices :: (BookInfo a) => a -> TVar AppState -> (UserAction -> IO ()) -> IO () initAppServices env stateTVar actionHandler = do _ <- async $ do - ispine <- runReaderT resolveSpine env - allChapters <- runReaderT (zipWithM getChapter ispine (map ChapterIndex [0 ..])) env + ispine <- getSpine env - let titles = metaTitles (eMetadata env) - let bookTitle = case find (\t -> titleType t == Just "main") titles of - Just t -> titleText t - Nothing -> if null titles then "Unknown" else titleText (head titles) + allChapters <- zipWithM (loadChapter env) ispine (map ChapterIndex [0 ..]) - atomically $ modifyTVar stateTVar $ \s -> s {cSpine = ispine, cAllChapters = allChapters, bTitle = T.pack bookTitle} + let bookTitle = getTitle env + + atomically $ modifyTVar stateTVar $ \s -> + s {cSpine = ispine, cAllChapters = allChapters, bTitle = bookTitle} _ <- GLib.idleAdd GLibConst.PRIORITY_DEFAULT $ do maybeSaved <- loadLastRead case maybeSaved of - Just (path, idx) | path == bookPath env -> actionHandler (LoadSpecific idx) + Just (path, idx) | path == getBookPath env -> actionHandler (LoadSpecific idx) _ -> actionHandler (LoadSpecific 0) pure False pure ()