import os from dataclasses import dataclass from datetime import datetime from enum import Enum from typing import Any, Mapping, Optional, Tuple, Union class Direction(Enum): FORWARD = "forward" BACKWARD = "backward" @dataclass(frozen=True) class BookMetadata: title: Optional[str] = None creator: Optional[str] = None description: Optional[str] = None publisher: Optional[str] = None date: Optional[str] = None language: Optional[str] = None format: Optional[str] = None identifier: Optional[str] = None source: Optional[str] = None @dataclass(frozen=True) class LibraryItem: last_read: datetime filepath: str title: Optional[str] = None author: Optional[str] = None reading_progress: Optional[float] = None def __str__(self) -> str: if self.reading_progress is None: reading_progress_str = "N/A" else: reading_progress_str = f"{int(self.reading_progress * 100)}%" reading_progress_str = reading_progress_str.rjust(4) book_name: str filename = self.filepath.replace(os.path.expanduser("~"), "~", 1) if self.title is not None and self.author is not None: book_name = f"{self.title} - {self.author} ({filename})" elif self.title is None and self.author: book_name = f"{filename} - {self.author}" else: book_name = filename last_read_str = self.last_read.strftime("%I:%M%p %b %d") return f"{reading_progress_str} {last_read_str}: {book_name}" @dataclass(frozen=True) class ReadingState: """ Data model for reading state. `row` has to be explicitly assigned with value because Seamless feature needs it to adjust from relative (to book's content index) row to absolute (to book's entire content) row. `rel_pctg` and `section` default to None and if either of them is assigned with value, then it will be overriding the `row` value. """ content_index: int textwidth: int row: int rel_pctg: Optional[float] = None section: Optional[str] = None @dataclass(frozen=True) class SearchData: direction: Direction = Direction.FORWARD value: str = "" @dataclass(frozen=True) class LettersCount: """ all: total letters in book cumulative: list of total letters for previous contents eg. let's say cumulative = (0, 50, 89, ...) it means 0 is total cumulative letters of book contents[-1] to contents[0] 50 is total cumulative letters of book contents[0] to contents[1] 89 is total cumulative letters of book contents[0] to contents[2] """ all: int cumulative: Tuple[int, ...] @dataclass(frozen=True) class CharPos: """ Describes character position in text. eg. ["Lorem ipsum dolor sit amet,", # row=0 "consectetur adipiscing elit."] # row=1 ^CharPos(row=1, col=3) """ row: int col: int @dataclass(frozen=True) class TextMark: """ Describes marking in text. eg. Interval [CharPos(row=0, col=3), CharPos(row=1, col=4)] notice the marking inclusive [] for both side instead of right exclusive [) """ start: CharPos end: Optional[CharPos] = None def is_valid(self) -> bool: """ Assert validity and check if the mark is unterminated eg.