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.
This is italic text
Missing tag """ if self.end is not None: if self.start.row == self.end.row: return self.start.col <= self.end.col else: return self.start.row < self.end.row return False @dataclass(frozen=True) class TextSpan: """ Like TextMark but using span of letters (n_letters) """ start: CharPos n_letters: int @dataclass(frozen=True) class InlineStyle: """ eg. InlineStyle(attr=curses.A_BOLD, row=3, cols=4, n_letters=3) """ row: int col: int n_letters: int attr: int @dataclass(frozen=True) class TocEntry: label: str content_index: int section: Optional[str] @dataclass(frozen=True) class TextStructure: """ Object that describes how the text should be displayed in screen. text_lines: ("list of lines", "of text", ...) image_maps: {line_num: path/to/image/in/ebook/zip} section_rows: {section_id: line_num} formatting: (InlineStyle, ...) """ text_lines: Tuple[str, ...] image_maps: Mapping[int, str] section_rows: Mapping[str, int] formatting: Tuple[InlineStyle, ...] @dataclass(frozen=True) class NoUpdate: pass class Key: """ Because ord("k") chr(34) are confusing """ def __init__(self, char_or_int: Union[str, int]): self.value: int = char_or_int if isinstance(char_or_int, int) else ord(char_or_int) self.char: str = char_or_int if isinstance(char_or_int, str) else chr(char_or_int) def __eq__(self, other: Any) -> bool: if isinstance(other, Key): return self.value == other.value return False def __ne__(self, other: Any) -> bool: return self.__eq__(other) def __hash__(self) -> int: return hash(self.value) class AppData: @property def prefix(self) -> Optional[str]: """Return None if there exists no homedir | userdir""" prefix: Optional[str] = None # UNIX filesystem homedir = os.getenv("HOME") # WIN filesystem userdir = os.getenv("USERPROFILE") if homedir: if os.path.isdir(os.path.join(homedir, ".config")): prefix = os.path.join(homedir, ".config", "epy") else: prefix = os.path.join(homedir, ".epy") elif userdir: prefix = os.path.join(userdir, ".epy") if prefix: os.makedirs(prefix, exist_ok=True) return prefix