implement generic bookinfo and move logic to parser

This commit is contained in:
Marko Andjelic 2026-02-12 12:47:01 +00:00
commit 80be08965e
3 changed files with 49 additions and 13 deletions

View file

@ -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

View file

@ -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)

View file

@ -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 ()