svitak/app/Types.hs
Marko Andjelic 81c2caaf67
add worker thread tracking
add stGen which gets bumped for every worker thread spawned by
loadBook

every thread captures stGen when it spawns and before actually loading
the book in the main thread, and in the worker thread it checks if
myGen == stGen

this throws away stale requests, meaning if you try and load another
book while one is still loading, it will ignore the one still loading
and start loading the latest request
2026-07-02 02:50:31 +01:00

126 lines
3.7 KiB
Haskell

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-- | Shared types for the whole reader. The parser ('EpubParser') produces a
-- 'BookStructure' cheaply and renders each 'Chapter' on demand; the UI
-- consumes both through the 'BookInfo' interface.
module Types
( -- * Identifiers
ChapterIndex (..),
InternalPath (..),
-- * Book content
Chapter (..),
ChapterRef (..),
TocEntry (..),
BookStructure (..),
-- * The book interface the UI talks to
BookInfo (..),
-- * UI state
AppState (..),
UserAction (..),
-- * EPUB environment
EpubEnv (..),
EpubAction,
)
where
import Codec.Archive.Zip (Archive)
import qualified Codec.Epub.Data.Manifest as DMan
import qualified Codec.Epub.Data.Metadata as DMeta
import Control.Monad.Except (ExceptT)
import Control.Monad.Reader (ReaderT)
import qualified Data.Map.Strict as M
import qualified Data.Text as T
-- | Position of a chapter within the linear reading order (the spine).
newtype ChapterIndex = ChapterIndex Int
deriving (Show, Eq, Ord, Num)
-- | A fully-resolved path to a file inside the EPUB zip.
newtype InternalPath = InternalPath FilePath
deriving (Show, Eq, Ord)
-- | A lightweight pointer to a chapter: enough to navigate and label it in the
-- sidebar without having rendered its (potentially large) content yet.
data ChapterRef = ChapterRef
{ refPath :: InternalPath,
refTitle :: T.Text,
refIndex :: ChapterIndex
}
deriving (Show)
-- | A chapter rendered to self-contained HTML, ready for WebKit.
data Chapter = Chapter
{ chapterPath :: InternalPath,
chapterTitle :: T.Text,
chapterHtml :: T.Text,
chapterIndex :: ChapterIndex
}
deriving (Show)
-- | A node in the (possibly nested) table of contents. @tocTarget@ is the
-- resolved chapter path; @tocFragment@ is the optional in-page anchor.
data TocEntry = TocEntry
{ tocLabel :: T.Text,
tocTarget :: InternalPath,
tocFragment :: T.Text,
tocChildren :: [TocEntry]
}
deriving (Show)
-- | The cheap-to-compute skeleton of a book: reading order + navigation tree.
data BookStructure = BookStructure
{ structRefs :: [ChapterRef],
structToc :: [TocEntry]
}
-- | The UI only ever sees a book through this interface, so it stays
-- format-agnostic. The single instance ('EpubEnv') lives in 'EpubParser'.
class BookInfo a where
bookTitle :: a -> T.Text
bookAuthors :: a -> [T.Text]
bookFilePath :: a -> FilePath
-- | Resolve the spine and TOC. Fast: does not render chapter bodies.
loadStructure :: a -> IO (Either String BookStructure)
-- | Render a single chapter's body to self-contained HTML, on demand.
renderChapter :: a -> ChapterRef -> IO (Either String Chapter)
-- | Mutable application state, held in a 'Control.Concurrent.STM.TVar'.
-- Rendered chapters accumulate in 'stCache' as the reader visits them.
data AppState = AppState
{ stRefs :: [ChapterRef],
stToc :: [TocEntry],
stCache :: M.Map ChapterIndex Chapter,
stIndex :: ChapterIndex,
stZoom :: Double,
stTitle :: T.Text,
stBookPath :: FilePath,
stGen :: Int
}
-- | A user request, produced by the keyboard / sidebar and handled centrally.
-- 'GoToChapter' carries an optional anchor to scroll to within the chapter.
data UserAction
= NextChapter
| PrevChapter
| ZoomIn
| ZoomOut
| GoToChapter ChapterIndex T.Text
-- | Parsed EPUB, carried through the parser as a reader environment.
data EpubEnv = EpubEnv
{ archive :: Archive,
opfXml :: String,
baseDir :: FilePath,
bookPath :: FilePath,
eMetadata :: DMeta.Metadata,
eManifest :: DMan.Manifest,
eVersion :: T.Text
}
-- | The parser monad: read-only access to the 'EpubEnv', with string errors.
type EpubAction a = ReaderT EpubEnv (ExceptT String IO) a