aboutsummaryrefslogtreecommitdiffstats
path: root/src/epy_reader/models.py
diff options
context:
space:
mode:
authorBenawi Adha <benawiadha@gmail.com>2022-10-02 21:22:38 +0700
committerBenawi Adha <benawiadha@gmail.com>2022-10-02 21:22:38 +0700
commit258c30d2e088cd4ab091a53794da3f93af79915d (patch)
treef49340bf565deb20c730358af74a01bcc231de53 /src/epy_reader/models.py
parentd43533f01d9d5baf5f78b71f832641382bd5962a (diff)
downloadepy-258c30d2e088cd4ab091a53794da3f93af79915d.tar.gz
Major refactor: breakdown epy.py script
into package project structure for easier development Squashed commit of the following: commit 01309b961a4ab32394bff0d90949b57435dfda47 Author: Benawi Adha <benawiadha@gmail.com> Date: Sun Oct 2 21:15:04 2022 +0700 Fix missing objects commit aab2e773c30b255c81b1250b3b20967d5da40338 Author: Benawi Adha <benawiadha@gmail.com> Date: Sun Oct 2 21:09:31 2022 +0700 Update README.md commit d4e98926bcd9b00ce0410ad71249d24e6315abc5 Author: Benawi Adha <benawiadha@gmail.com> Date: Sun Oct 2 21:07:28 2022 +0700 Add keywords in pyproject.toml commit 432055af8245560a3ff2e046aef0b4e87da44930 Author: Benawi Adha <benawiadha@gmail.com> Date: Sun Oct 2 21:04:34 2022 +0700 Bump version and deprecete setup.py commit 51dd15aab8f8ff5996f822f8378e813f0b9fb80d Author: Benawi Adha <benawiadha@gmail.com> Date: Sun Oct 2 20:56:38 2022 +0700 Formatting commit 81fb35e3b6fa0e27d79ef1da77202ed81eb99500 Author: Benawi Adha <benawiadha@gmail.com> Date: Sun Oct 2 20:55:08 2022 +0700 Fix speakers module commit 3b852e7c59b38d5a28520038e35f50a95270d2f1 Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 20:52:46 2022 +0700 Fix circular import commit 061e8a2649dabacd28a9e2f972559475316c654c Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 20:39:27 2022 +0700 Run formatting commit abc2d0ab156992c63dc04745d14a69679a60accb Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 20:39:00 2022 +0700 Update isort and black config in pyproject commit 5dc2e41bab5b997bd719bdc1561eb51ba0c17a83 Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 20:31:00 2022 +0700 Add app Config commit ed485a2ea8281585bf86dc5772f0c6dd9c803cc4 Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 20:23:02 2022 +0700 Update debugpy script commit 68b0553dd4d63eb4b847132c68ea4018587fa8ec Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 20:14:11 2022 +0700 Connect reader to main script commit 63c3dd176f18a784a4ed2e88aa72b13d1c2b0990 Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 20:11:17 2022 +0700 Implement reader commit ce5eec8fb4e1db3870a16a07541365cd777d6c4c Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 19:29:49 2022 +0700 Fix script in pyproject.toml commit 941e8e49f1593731fb582d92084206772b3f0442 Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 19:28:39 2022 +0700 Rename modules commit 5a3e7f766aee774c09b3b5336f3a2968e9cb1d0c Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 19:28:20 2022 +0700 Rename tool method commit 3c0503ff475cb7eff8b12d3be0bda7a38efe1072 Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 19:27:03 2022 +0700 Add ebooks lib commit b5f71c3296a7d6f36454f6e1cbe84e15a45092ee Author: Benawi Adha <benawiadha@gmail.com> Date: Sat Oct 1 17:25:11 2022 +0700 Initial reorganization
Diffstat (limited to 'src/epy_reader/models.py')
-rw-r--r--src/epy_reader/models.py232
1 files changed, 232 insertions, 0 deletions
diff --git a/src/epy_reader/models.py b/src/epy_reader/models.py
new file mode 100644
index 0000000..db4701b
--- /dev/null
+++ b/src/epy_reader/models.py
@@ -0,0 +1,232 @@
+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. <div><i>This is italic text</div>
+ Missing </i> 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