171 Commits
0.2 ... master

Author SHA1 Message Date
BasioMeusPuga
4389a0f5aa Merge pull request #120 from terrycloth/packaging
AppStream metadata, for giving Lector a proper page in Linux app stores
2020-01-15 00:08:44 +05:30
Andrew Toskin
b54ff37828 Replace empty dummy text with actual release notes, at least for now
We don't necessarily *need* to maintain release notes in the
.metainfo.xml file, but for my local test package builds, it would be
better to have actual information than to leave in the dummy text. (The
duplicate empty versions cause it to fail validation.)
2020-01-13 11:02:32 -08:00
Andrew Toskin
e7dd10fa3a Preferred naming scheme for .desktop files is also "reverse-DNS" 2019-12-30 20:40:40 -08:00
Andrew Toskin
3ede5b78fa Add AppStream .metainfo.xml file for Linux app stores
I had to make a number of assumptions here, like what the ID and
metainfo.xml license should be, whether to include the releases, etc. I
think it will be easier to talk about them in the pull request, though,
as then I'll be able to highlight the relevant lines for each point.
2019-12-30 20:37:19 -08:00
BasioMeusPuga
418d9e0c1c Merge pull request #116 from timgates42/bugfix/typo_specific
Fix simple typo: specifc -> specific
2019-12-02 22:33:28 -08:00
Tim Gates
e977826ea1 Fix simple typo: specifc -> specific 2019-12-03 15:05:57 +11:00
BasioMeusPuga
c661ed54de Update translations: Portuguese 2019-10-31 09:10:17 -07:00
BasioMeusPuga
dd3aa8a49c Merge pull request #112 from elisamalzoni/master
Portuguese support
2019-10-31 08:59:18 -07:00
elisamalzoni
65ad48c442 Portuguese support 2019-10-29 09:44:32 -03:00
BasioMeusPuga
fd433d6432 Tag generation stopgap 2019-08-26 17:42:56 -07:00
BasioMeusPuga
2bc73450fe Update translations: Czech 2019-07-06 21:47:45 -07:00
BasioMeusPuga
8b6800e14f Merge pull request #104 from VirtualThief/txt-support
Support for TXT files
2019-07-06 04:42:29 -07:00
Dmitrii Petukhov
d0cdd531a9 Check for textile installation 2019-07-06 07:37:32 +01:00
Dmitrii Petukhov
5e74b6f261 Fix for bookmarks in books without cover 2019-07-06 07:30:40 +01:00
Dmitrii Petukhov
24e45ac2b7 Support for opening txt files 2019-07-06 00:09:50 +01:00
BasioMeusPuga
916bdb5b14 Update translations: Japanese 2019-05-26 07:43:12 -04:00
BasioMeusPuga
ca108da948 Merge pull request #100 from sorairolake/japanese-translation
Add Japanese translation
2019-05-26 07:36:29 -04:00
Shun Sakai
6762f2cfce Add Japanese translation 2019-05-24 22:02:10 +09:00
BasioMeusPuga
0aea9ec33b Implement text pagination
Start double page mode
2019-04-01 22:25:09 -04:00
BasioMeusPuga
af1b988d93 Minor fixes 2019-03-22 21:35:09 -04:00
BasioMeusPuga
8fd6a0d432 Improve navigation bar 2019-03-16 21:53:19 -04:00
BasioMeusPuga
56f15528c2 Markdown support 2019-03-16 12:02:30 -04:00
BasioMeusPuga
f358ad169c Preliminary Navigation Bar 2019-03-16 10:27:07 -04:00
BasioMeusPuga
4cf0a9e78c Implement image rotation 2019-03-16 00:06:53 -04:00
BasioMeusPuga
38de0dcd13 Improve DjVu support 2019-03-15 19:08:06 -04:00
BasioMeusPuga
eb49ca92a4 Update README.md 2019-03-14 23:07:37 -04:00
BasioMeusPuga
bf93c7beab Preliminary DjVu support
SideDock fade in animation
2019-03-14 23:00:42 -04:00
BasioMeusPuga
ca57983739 Fix cover image name assignment 2019-03-11 07:06:54 -04:00
BasioMeusPuga
c71985f621 Minor fixes 2019-03-09 10:11:12 -05:00
BasioMeusPuga
c8fe0ba8b6 Make dependency checks less... crashy 2019-03-03 07:56:05 -05:00
BasioMeusPuga
d6df28c503 Merge pull request #92 from guoyunhe/hidpi-icons
Make icons sharp in HiDPI screen
2019-03-02 19:47:50 -05:00
Guo Yunhe
75ace25c57 Make icons sharp in HiDPI screen 2019-03-02 13:37:04 +01:00
BasioMeusPuga
d2d7dc2c8f Update for release 2019-03-01 23:17:22 -05:00
BasioMeusPuga
f622b0c23e Implement image color inversion 2019-02-19 00:01:07 +05:30
BasioMeusPuga
f312714a2c Multiple fixes
MuPDF import error
Definition text color
Database logging
2019-02-15 00:17:47 +05:30
BasioMeusPuga
c3f26ca225 Whoops 2019-02-13 00:27:48 +05:30
BasioMeusPuga
f6c7307647 Update README.md 2019-02-13 00:12:01 +05:30
BasioMeusPuga
b1714b9674 Error notifications
In application log viewer / database reset
Cleanup settings navigation
2019-02-13 00:09:37 +05:30
BasioMeusPuga
fa030e3060 Update flatpak manifest
Implement automated missing cover downloading
2019-02-12 11:51:16 +05:30
BasioMeusPuga
564db06179 Multiple fixes
Images are now center aligned
Better logging
2019-02-11 11:54:05 +05:30
BasioMeusPuga
3cd75807f9 Fix MOBI parser
Update Kindleunpack
Discover new and exciting bugs
2019-02-10 17:58:35 +05:30
BasioMeusPuga
f6f9d01060 Cleanup parsers 2019-02-10 09:03:12 +05:30
BasioMeusPuga
c6e30b67ad Improve EPUB parser compatibility and speed
Completely break MOBI parser
2019-02-10 06:47:51 +05:30
BasioMeusPuga
e4be239bf0 Overhaul EPUB parsing and ToC generation 2019-02-09 04:21:22 +05:30
BasioMeusPuga
1e004774c9 Search Google books for missing covers
Small fixes
2019-02-05 23:12:22 +05:30
BasioMeusPuga
91ca1e2190 Improve fb2 parsing
Miscellaneous fixes to navigation
2019-02-05 02:50:47 +05:30
BasioMeusPuga
d1662b47d9 Small fixes 2019-02-02 13:04:38 +05:30
BasioMeusPuga
dfe0fceea9 Small fixes
Compulsive refactor
2019-01-31 13:00:21 +05:30
BasioMeusPuga
268014cc3a Refactor sideDock 2019-01-31 01:51:47 +05:30
BasioMeusPuga
d1b1d7c59c Cleanup 2019-01-29 07:32:52 +05:30
BasioMeusPuga
470fc1078f Multiple fixes
Update translations
2019-01-28 02:28:43 +05:30
BasioMeusPuga
96f4d9193a Update requirements.txt 2019-01-26 19:53:40 +05:30
BasioMeusPuga
7aa42603bd Debulk widgets module 2019-01-26 19:44:58 +05:30
BasioMeusPuga
9a6392d1e6 Update README.md 2019-01-26 19:42:33 +05:30
BasioMeusPuga
739b84e9f4 Overhaul TOC generation and navigation 2019-01-26 19:03:30 +05:30
BasioMeusPuga
66746b4eaa Improve cover creation for PDFs 2019-01-22 23:32:25 +05:30
BasioMeusPuga
164450a888 Shift to MuPDF backend for pdf rendering 2019-01-22 22:36:32 +05:30
BasioMeusPuga
191ea7ef3a Update README.md 2019-01-19 22:34:21 +05:30
BasioMeusPuga
ca8ddd38a2 Improve logging
requirements.txt
Small UI fixes
2019-01-19 22:29:56 +05:30
BasioMeusPuga
a45e183914 Implement logging 2019-01-19 20:31:19 +05:30
BasioMeusPuga
506c458544 Update readme
Begin logging
Account for fb2 books without covers
2019-01-19 01:19:58 +05:30
BasioMeusPuga
5e3987dc04 Improve comic view 2019-01-17 23:03:28 +05:30
BasioMeusPuga
5b8bc1d707 Comic page increment setting 2019-01-17 22:42:11 +05:30
BasioMeusPuga
b3e4060661 UI cleanup 2019-01-17 22:17:22 +05:30
BasioMeusPuga
2185e9fcf7 Tab reordering 2019-01-17 21:53:04 +05:30
BasioMeusPuga
5d35319164 Improve mouse pointer hiding
Improve search result formatting
2019-01-16 12:58:40 +05:30
BasioMeusPuga
c6d24fd970 Multiple fixes 2019-01-16 12:25:59 +05:30
BasioMeusPuga
17f39c557b Manga mode
Comics are parsed for images only
Miscellaneous fixes
2019-01-14 15:54:29 +05:30
BasioMeusPuga
f997bc9c9a Search result highlighting
Disable UI elements when irrelevant
2019-01-09 13:50:08 +05:30
BasioMeusPuga
930a97a8fa Implement search 2019-01-09 05:58:47 +05:30
BasioMeusPuga
026fff3d7a Implement search UI
Discovered severe inadequacies. Some of them were in the program.
2019-01-08 05:47:50 +05:30
BasioMeusPuga
f9eec130dd Update translations 2019-01-03 04:05:52 +05:30
BasioMeusPuga
d75689ea97 Consolidate docks
Update copyright
Pondered the nature of protein powder
2019-01-03 03:28:31 +05:30
BasioMeusPuga
6ea5635d28 Cleanup 2018-12-01 18:59:49 +05:30
BasioMeusPuga
4b9221128c Implement single/double page modes for comics/pdfs 2018-10-31 10:39:21 +05:30
BasioMeusPuga
b5349315be UI elements for page modes 2018-10-29 23:08:34 +05:30
BasioMeusPuga
ee18f157f1 Account for absence of Qt Multimedia 2018-10-20 03:49:45 +05:30
BasioMeusPuga
ae325736d5 Update README.md 2018-07-11 13:09:16 -04:00
BasioMeusPuga
826da72d4b Adjust image paths 2018-07-11 13:07:58 -04:00
BasioMeusPuga
b5231cd383 Merge pull request #71 from psikoz/master
logo add
2018-07-11 13:02:24 -04:00
BasioMeusPuga
5ac843e48c Merge pull request #72 from guoyunhe/patch-1
Update README: openSUSE package is now official
2018-07-11 12:55:22 -04:00
Guo Yunhe
74417319be Update README: openSUSE package is now official
Lector is now included in openSUSE Tumbleweed and will be included in future Leap releases.
2018-07-11 13:15:37 +03:00
psikoz
f8555a6ed5 Update README.md 2018-07-11 12:33:03 +03:00
psikoz
4346c27adc Add files via upload 2018-07-11 12:21:09 +03:00
BasioMeusPuga
16adf57dae Add option to include TOC with bookmarks
Shift dock positions
2018-07-08 20:20:57 -04:00
BasioMeusPuga
8534088f4a Update README.md 2018-07-06 16:19:24 -04:00
BasioMeusPuga
a81ed537a6 Improve bookmark addition and deletion
Fix toolbar button checking
2018-07-06 15:38:56 -04:00
BasioMeusPuga
ed5bc0b2b9 Shift to tree view for bookmarks
Cleanup
2018-07-06 09:01:40 -04:00
BasioMeusPuga
8cb8904e58 Exception handling for improperly formatted fb2 books 2018-06-17 10:40:37 -04:00
BasioMeusPuga
aa093b8cc2 Image display in fb2 parser 2018-06-17 10:29:55 -04:00
BasioMeusPuga
42b4d0317d Update README.md 2018-06-14 16:14:11 -04:00
BasioMeusPuga
a0e463bc58 Speed up file addition
Improve fb2 parser
Fix extension checking
2018-06-14 16:10:27 -04:00
BasioMeusPuga
4a2da61b51 Merge branch 'master' of https://github.com/basiomeuspuga/lector 2018-06-13 16:33:57 -04:00
BasioMeusPuga
5c481ccafe Begin fb2 support
Fix Chinese translation
2018-06-13 16:33:30 -04:00
BasioMeusPuga
30760b879e Merge pull request #67 from guoyunhe/guoyunhe-patch-1
Add openSUSE package
2018-06-07 18:17:41 -04:00
Guo Yunhe
af7868f62a Add openSUSE package 2018-06-07 19:03:45 +03:00
BasioMeusPuga
62c44730d8 Start search functionality
Multiple fixes
2018-05-24 15:14:56 -04:00
BasioMeusPuga
045d8a3e52 Update README.md 2018-05-15 00:45:40 -04:00
BasioMeusPuga
55bee210c6 Update version for release
Update translations
2018-05-13 18:40:24 -04:00
BasioMeusPuga
d3746c8e98 Merge branch 'master' of https://github.com/basiomeuspuga/lector 2018-05-13 18:28:52 -04:00
BasioMeusPuga
ffcf07414f Implement file drag drop 2018-05-13 18:16:17 -04:00
BasioMeusPuga
ffaace2eaa Uniform tab sizes
Path search
PDF parser exception handling
2018-05-13 15:54:17 -04:00
BasioMeusPuga
32455dd859 Merge pull request #52 from jaccsr/master
Language code
2018-05-05 09:38:19 -04:00
BasioMeusPuga
3e54340694 Account for older versions of Qt 2018-05-03 08:37:22 -04:00
jaccsr
bc6c7d1c36 I forgot the language code 2018-05-02 10:23:41 +08:00
jaccsr
ebd746b7b2 Merge pull request #1 from BasioMeusPuga/master
a
2018-05-02 09:11:23 +08:00
BasioMeusPuga
ebc3ef9f1b Update README.md 2018-05-01 18:33:44 -04:00
BasioMeusPuga
7238605441 Update translations: Chinese (simplified) 2018-05-01 18:29:59 -04:00
BasioMeusPuga
ea86737970 Merge pull request #50 from jaccsr/master
Chinese (simplified) translation
2018-05-01 18:22:19 -04:00
jaccsr
ab4c586c06 add chinese(simp) language 2018-05-02 06:06:23 +08:00
BasioMeusPuga
7977bde410 Multiple fixes 2018-04-29 08:11:46 -04:00
BasioMeusPuga
626472dd04 Comic view drag and drop
Menu icons
Polish for docks
2018-04-20 10:00:12 +05:30
BasioMeusPuga
d9efe2da3c Annotation notes 2018-04-19 20:35:22 +05:30
BasioMeusPuga
ec197f0829 Annotation saving, loading, and deletion 2018-04-19 15:19:20 +05:30
BasioMeusPuga
335479bcfb Small fixes 2018-04-17 11:24:16 +05:30
BasioMeusPuga
cbf01c6d16 Annotation placement 2018-04-16 13:00:49 +05:30
BasioMeusPuga
98ca118a60 Remove unnecessary shebangs
How this isn't a Ricky Martin song, we'll never know
2018-04-12 10:15:20 +05:30
BasioMeusPuga
c7aa0e28ee Web search for selection
Bugfixes
2018-04-11 01:43:21 +05:30
BasioMeusPuga
528c2e387c Improve spacebar navigation
Refactor variables
2018-04-10 12:39:52 +05:30
BasioMeusPuga
bc54d6b686 Complete annotation editor
Annotation saving and loading
2018-04-08 14:41:31 +05:30
BasioMeusPuga
8f298de58e Cleanup optional imports
Disable the multiprocessing module on Windows
Update translations
2018-04-02 19:57:46 +05:30
BasioMeusPuga
366859ebe0 Fix incorrect function argument 2018-04-02 01:06:53 +05:30
BasioMeusPuga
8c51cc047e Split content display widgets into new module 2018-03-31 10:43:13 +05:30
BasioMeusPuga
5081a31f1a Fine tune progress display
Option: Set consider read at percentage
Small fixes
2018-03-31 10:31:57 +05:30
BasioMeusPuga
aff69d95c1 Make progress work with block count
Break database thoroughly
Fix pdf year bug
2018-03-31 03:03:49 +05:30
BasioMeusPuga
0b8427c864 Flatpak manifest: No pdf support but otherwise functional 2018-03-30 21:03:37 +05:30
BasioMeusPuga
43dd6a34d9 Implement basic annotation editor / preview 2018-03-30 20:48:13 +05:30
BasioMeusPuga
2f4adfc183 Small fixes 2018-03-30 11:06:48 +05:30
BasioMeusPuga
0d015ad72e Auto hide Tab-bar and Statusbar 2018-03-29 02:50:01 +05:30
BasioMeusPuga
406ca0485f Position setting should work all the time now
Learn not to swear so much at the screen
Cover icons in the tab bar
Shift Scan Library button from the Library tab to the Library toolbar
2018-03-29 01:45:58 +05:30
BasioMeusPuga
ab6760226e Search position seeking fix for multiple tabs
Space navigation tries its best to not cut lines off
2018-03-28 20:17:00 +05:30
BasioMeusPuga
66c8626d43 Significant improvements to bookmark dock display 2018-03-28 00:46:12 +05:30
BasioMeusPuga
5fa724ae69 Implement scroll speed slider 2018-03-27 21:58:35 +05:30
BasioMeusPuga
d417a94829 Fix fullscreen toggle affecting reading position bug
Bookmark navigation much more reliable
Start annotations UI
2018-03-27 08:23:07 +05:30
BasioMeusPuga
9c85a1075e Fix context menu behavior 2018-03-24 01:34:42 +05:30
BasioMeusPuga
dd4b502861 Move contentView profile modification functions to guifunctions module 2018-03-24 01:15:33 +05:30
BasioMeusPuga
0f963b20f9 Move cover loading and culling to guifunctions module 2018-03-24 00:30:58 +05:30
BasioMeusPuga
f63b6627b2 Update README.md 2018-03-24 00:00:12 +05:30
BasioMeusPuga
00db5d5e0f Update translations 2018-03-23 23:57:49 +05:30
BasioMeusPuga
5e53d40e68 Redesign settings dialog
Remove dependency on requests
2018-03-23 23:56:01 +05:30
BasioMeusPuga
6ffa6934ed Fix splitting for a repeated anchor 2018-03-23 17:48:58 +05:30
BasioMeusPuga
34dcf9f1b4 Fix empty database triggering error at first start 2018-03-23 15:15:42 +05:30
BasioMeusPuga
7931f92335 Application icon and .desktop file
Rearrange modules because of single-version-externally-managed
2018-03-23 00:58:42 +05:30
BasioMeusPuga
42b655862c Partially fix tab close memory leak 2018-03-22 19:06:16 +05:30
BasioMeusPuga
e6eb056ec6 Make context menus more coherent
Update translations
2018-03-22 14:51:33 +05:30
BasioMeusPuga
7f5b6fc349 Multiple UI improvements 2018-03-21 21:19:28 +05:30
BasioMeusPuga
9af175b11f Merge pull request #28 from szymonpk/gentoo-ebuild
Add link to unofficial Gentoo ebuild
2018-03-21 15:31:50 +05:30
BasioMeusPuga
c783e44444 Adjust widgets to screen size
Delete key for the library
2018-03-21 15:28:58 +05:30
BasioMeusPuga
a1dba753e8 Manually added books no longer removed on library refresh
Overhaul database module
2018-03-21 15:04:28 +05:30
Szymon Szypulski
a55a0e7205 Add link to unofficial Gentoo ebuild 2018-03-21 07:31:15 +01:00
BasioMeusPuga
bb8de60efe Fix books in subdirectories getting filtered 2018-03-20 20:36:24 +05:30
BasioMeusPuga
0d8c2b6648 Multiple fixes
Update translations
2018-03-20 13:24:17 +05:30
BasioMeusPuga
64a96d816d Update translations: German 2018-03-20 08:26:35 +05:30
BasioMeusPuga
5a4af54118 Merge pull request #25 from atmaxinger/master
German translation
2018-03-20 08:19:01 +05:30
atmaxinger
4cf18e008d German translation 2018-03-20 00:10:05 +01:00
BasioMeusPuga
50cc52b116 Update README.md 2018-03-20 00:04:05 +05:30
BasioMeusPuga
35f38b9f68 French translation 2018-03-19 23:57:47 +05:30
BasioMeusPuga
c883ba0175 Merge pull request #24 from eclipseo/add_French_translation
Great work! 
Give me a minute to update the binary files.
2018-03-19 23:52:50 +05:30
BasioMeusPuga
39cf03a70e Switch context menu TOC to combobox
Update translations
2018-03-19 23:40:16 +05:30
Robert-André Mauchin
44d88d99bb Add French translation
Signed-off-by: Robert-André Mauchin <zebob.m@gmail.com>
2018-03-19 18:36:10 +01:00
BasioMeusPuga
ca67071e91 Add TOC to context menu in distraction free mode 2018-03-19 19:28:33 +05:30
BasioMeusPuga
b5acce6449 Small fixes 2018-03-19 18:26:43 +05:30
BasioMeusPuga
7bdf01a67e Spanish translation 2018-03-19 17:48:25 +05:30
BasioMeusPuga
aca08827fb Implement save as for comic/pdf view
Account for malformed container.xml for epubs
2018-03-19 01:11:55 +05:30
BasioMeusPuga
d4aaa4dc74 Update README.md 2018-03-19 00:43:18 +05:30
BasioMeusPuga
98daa40bfd Implement internationalization support 2018-03-19 00:11:06 +05:30
BasioMeusPuga
a7df896468 Mark translatable strings 2018-03-18 22:19:19 +05:30
BasioMeusPuga
fd149dcafa Usability improvements
Keyboard shortcuts
Title reporting
Context menu for comic/pdf view
2018-03-18 01:19:04 +05:30
BasioMeusPuga
0bb2e9329f Fix fullscreened widget not finding main window 2018-03-17 12:56:23 +05:30
BasioMeusPuga
89a32bfeda Add toggle for image caching
Remove PyQt5 reference from setup.py
2018-03-17 10:44:02 +05:30
BasioMeusPuga
50089cb57a Remove version requirements 2018-03-17 00:38:09 +05:30
214 changed files with 35216 additions and 8041 deletions

View File

@@ -1,525 +0,0 @@
#! /usr/bin/python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# this program works in concert with the output from KindleUnpack
'''
Convert from Mobi ML to XHTML
'''
import os
import sys
import re
SPECIAL_HANDLING_TAGS = {
'?xml' : ('xmlheader', -1),
'!--' : ('comment', -3),
'!DOCTYPE' : ('doctype', -1),
}
SPECIAL_HANDLING_TYPES = ['xmlheader', 'doctype', 'comment']
SELF_CLOSING_TAGS = ['br' , 'hr', 'input', 'img', 'image', 'meta', 'spacer', 'link', 'frame', 'base', 'col', 'reference']
class MobiMLConverter(object):
PAGE_BREAK_PAT = re.compile(r'(<[/]{0,1}mbp:pagebreak\s*[/]{0,1}>)+', re.IGNORECASE)
IMAGE_ATTRS = ('lowrecindex', 'recindex', 'hirecindex')
def __init__(self, filename):
self.base_css_rules = 'blockquote { margin: 0em 0em 0em 1.25em }\n'
self.base_css_rules += 'p { margin: 0em }\n'
self.base_css_rules += '.bold { font-weight: bold }\n'
self.base_css_rules += '.italic { font-style: italic }\n'
self.base_css_rules += '.mbp_pagebreak { page-break-after: always; margin: 0; display: block }\n'
self.tag_css_rules = {}
self.tag_css_rule_cnt = 0
self.path = []
self.filename = filename
self.wipml = open(self.filename, 'rb').read()
self.pos = 0
self.opfname = self.filename.rsplit('.',1)[0] + '.opf'
self.opos = 0
self.meta = ''
self.cssname = os.path.join(os.path.dirname(self.filename),'styles.css')
self.current_font_size = 3
self.font_history = []
def cleanup_html(self):
self.wipml = re.sub(r'<div height="0(pt|px|ex|em|%){0,1}"></div>', '', self.wipml)
self.wipml = self.wipml.replace('\r\n', '\n')
self.wipml = self.wipml.replace('> <', '>\n<')
self.wipml = self.wipml.replace('<mbp: ', '<mbp:')
# self.wipml = re.sub(r'<?xml[^>]*>', '', self.wipml)
self.wipml = self.wipml.replace('<br></br>','<br/>')
def replace_page_breaks(self):
self.wipml = self.PAGE_BREAK_PAT.sub(
'<div class="mbp_pagebreak" />',
self.wipml)
# parse leading text of ml and tag
def parseml(self):
p = self.pos
if p >= len(self.wipml):
return None
if self.wipml[p] != '<':
res = self.wipml.find('<',p)
if res == -1 :
res = len(self.wipml)
self.pos = res
return self.wipml[p:res], None
# handle comment as a special case to deal with multi-line comments
if self.wipml[p:p+4] == '<!--':
te = self.wipml.find('-->',p+1)
if te != -1:
te = te+2
else :
te = self.wipml.find('>',p+1)
ntb = self.wipml.find('<',p+1)
if ntb != -1 and ntb < te:
self.pos = ntb
return self.wipml[p:ntb], None
self.pos = te + 1
return None, self.wipml[p:te+1]
# parses string version of tag to identify its name,
# its type 'begin', 'end' or 'single',
# plus build a hashtable of its attributes
# code is written to handle the possiblity of very poor formating
def parsetag(self, s):
p = 1
# get the tag name
tname = None
ttype = None
tattr = {}
while s[p:p+1] == ' ' :
p += 1
if s[p:p+1] == '/':
ttype = 'end'
p += 1
while s[p:p+1] == ' ' :
p += 1
b = p
while s[p:p+1] not in ('>', '/', ' ', '"', "'", "\r", "\n") :
p += 1
tname=s[b:p].lower()
if tname == '!doctype':
tname = '!DOCTYPE'
# special cases
if tname in SPECIAL_HANDLING_TAGS.keys():
ttype, backstep = SPECIAL_HANDLING_TAGS[tname]
tattr['special'] = s[p:backstep]
if ttype is None:
# parse any attributes
while s.find('=',p) != -1 :
while s[p:p+1] == ' ' :
p += 1
b = p
while s[p:p+1] != '=' :
p += 1
aname = s[b:p].lower()
aname = aname.rstrip(' ')
p += 1
while s[p:p+1] == ' ' :
p += 1
if s[p:p+1] in ('"', "'") :
p = p + 1
b = p
while s[p:p+1] not in ('"', "'") :
p += 1
val = s[b:p]
p += 1
else :
b = p
while s[p:p+1] not in ('>', '/', ' ') :
p += 1
val = s[b:p]
tattr[aname] = val
# label beginning and single tags
if ttype is None:
ttype = 'begin'
if s.find(' /',p) >= 0:
ttype = 'single_ext'
elif s.find('/',p) >= 0:
ttype = 'single'
return ttype, tname, tattr
# main routine to convert from mobi markup language to html
def processml(self):
# are these really needed
html_done = False
head_done = False
body_done = False
skip = False
htmlstr = ''
self.replace_page_breaks()
self.cleanup_html()
# now parse the cleaned up ml into standard xhtml
while True:
r = self.parseml()
if not r:
break
text, tag = r
if text:
if not skip:
htmlstr += text
if tag:
ttype, tname, tattr = self.parsetag(tag)
# If we run into a DTD or xml declarations inside the body ... bail.
if tname in SPECIAL_HANDLING_TAGS.keys() and tname != 'comment' and body_done:
htmlstr += '\n</body></html>'
break
# make sure self-closing tags actually self-close
if ttype == 'begin' and tname in SELF_CLOSING_TAGS:
ttype = 'single'
# make sure any end tags of self-closing tags are discarded
if ttype == 'end' and tname in SELF_CLOSING_TAGS:
continue
# remove embedded guide and refernces from old mobis
if tname in ('guide', 'ncx', 'reference') and ttype in ('begin', 'single', 'single_ext'):
tname = 'removeme:{0}'.format(tname)
tattr = None
if tname in ('guide', 'ncx', 'reference', 'font', 'span') and ttype == 'end':
if self.path[-1] == 'removeme:{0}'.format(tname):
tname = 'removeme:{0}'.format(tname)
tattr = None
# Get rid of font tags that only have a color attribute.
if tname == 'font' and ttype in ('begin', 'single', 'single_ext'):
if 'color' in tattr.keys() and len(tattr.keys()) == 1:
tname = 'removeme:{0}'.format(tname)
tattr = None
# Get rid of empty spans in the markup.
if tname == 'span' and ttype in ('begin', 'single', 'single_ext') and not len(tattr):
tname = 'removeme:{0}'.format(tname)
# need to handle fonts outside of the normal methods
# so fonts tags won't be added to the self.path since we keep track
# of font tags separately with self.font_history
if tname == 'font' and ttype == 'begin':
# check for nested font start tags
if len(self.font_history) > 0 :
# inject a font end tag
taginfo = ('end', 'font', None)
htmlstr += self.processtag(taginfo)
self.font_history.append((ttype, tname, tattr))
# handle the current font start tag
taginfo = (ttype, tname, tattr)
htmlstr += self.processtag(taginfo)
continue
# check for nested font tags and unnest them
if tname == 'font' and ttype == 'end':
self.font_history.pop()
# handle this font end tag
taginfo = ('end', 'font', None)
htmlstr += self.processtag(taginfo)
# check if we were nested
if len(self.font_history) > 0:
# inject a copy of the most recent font start tag from history
taginfo = self.font_history[-1]
htmlstr += self.processtag(taginfo)
continue
# keep track of nesting path
if ttype == 'begin':
self.path.append(tname)
elif ttype == 'end':
if tname != self.path[-1]:
print ('improper nesting: ', self.path, tname, ttype)
if tname not in self.path:
# handle case of end tag with no beginning by injecting empty begin tag
taginfo = ('begin', tname, None)
htmlstr += self.processtag(taginfo)
print(" - fixed by injecting empty start tag ", tname)
self.path.append(tname)
elif len(self.path) > 1 and tname == self.path[-2]:
# handle case of dangling missing end
taginfo = ('end', self.path[-1], None)
htmlstr += self.processtag(taginfo)
print(" - fixed by injecting end tag ", self.path[-1])
self.path.pop()
self.path.pop()
if tname == 'removeme:{0}'.format(tname):
if ttype in ('begin', 'single', 'single_ext'):
skip = True
else:
skip = False
else:
taginfo = (ttype, tname, tattr)
htmlstr += self.processtag(taginfo)
# handle potential issue of multiple html, head, and body sections
if tname == 'html' and ttype == 'begin' and not html_done:
htmlstr += '\n'
html_done = True
if tname == 'head' and ttype == 'begin' and not head_done:
htmlstr += '\n'
# also add in metadata and style link tags
htmlstr += self.meta
htmlstr += '<link href="styles.css" rel="stylesheet" type="text/css" />\n'
head_done = True
if tname == 'body' and ttype == 'begin' and not body_done:
htmlstr += '\n'
body_done = True
# handle issue of possibly missing html, head, and body tags
# I have not seen this but the original did something like this so ...
if not body_done:
htmlstr = '<body>\n' + htmlstr + '</body>\n'
if not head_done:
headstr = '<head>\n'
headstr += self.meta
headstr += '<link href="styles.css" rel="stylesheet" type="text/css" />\n'
headstr += '</head>\n'
htmlstr = headstr + htmlstr
if not html_done:
htmlstr = '<html>\n' + htmlstr + '</html>\n'
# finally add DOCTYPE info
htmlstr = '<?xml version="1.0"?>\n<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n' + htmlstr
css = self.base_css_rules
for cls, rule in self.tag_css_rules.items():
css += '.%s { %s }\n' % (cls, rule)
return (htmlstr, css, self.cssname)
def ensure_unit(self, raw, unit='px'):
if re.search(r'\d+$', raw) is not None:
raw += unit
return raw
# flatten possibly modified tag back to string
def taginfo_tostring(self, taginfo):
(ttype, tname, tattr) = taginfo
if ttype is None or tname is None:
return ''
if ttype == 'end':
return '</%s>' % tname
if ttype in SPECIAL_HANDLING_TYPES and tattr is not None and 'special' in tattr.keys():
info = tattr['special']
if ttype == 'comment':
return '<%s %s-->' % tname, info
else:
return '<%s %s>' % tname, info
res = []
res.append('<%s' % tname)
if tattr is not None:
for key in tattr.keys():
res.append(' %s="%s"' % (key, tattr[key]))
if ttype == 'single':
res.append('/>')
elif ttype == 'single_ext':
res.append(' />')
else :
res.append('>')
return "".join(res)
# routines to convert from mobi ml tags atributes to xhtml attributes and styles
def processtag(self, taginfo):
# Converting mobi font sizes to numerics
size_map = {
'xx-small': '1',
'x-small': '2',
'small': '3',
'medium': '4',
'large': '5',
'x-large': '6',
'xx-large': '7',
}
size_to_em_map = {
'1': '.65em',
'2': '.75em',
'3': '1em',
'4': '1.125em',
'5': '1.25em',
'6': '1.5em',
'7': '2em',
}
# current tag to work on
(ttype, tname, tattr) = taginfo
if not tattr:
tattr = {}
styles = []
if tname is None or tname.startswith('removeme'):
return ''
# have not seen an example of this yet so keep it here to be safe
# until this is better understood
if tname in ('country-region', 'place', 'placetype', 'placename',
'state', 'city', 'street', 'address', 'content'):
tname = 'div' if tname == 'content' else 'span'
for key in tattr.keys():
tattr.pop(key)
# handle general case of style, height, width, bgcolor in any tag
if 'style' in tattr.keys():
style = tattr.pop('style').strip()
if style:
styles.append(style)
if 'align' in tattr.keys():
align = tattr.pop('align').strip()
if align:
if tname in ('table', 'td', 'tr'):
pass
else:
styles.append('text-align: %s' % align)
if 'height' in tattr.keys():
height = tattr.pop('height').strip()
if height and '<' not in height and '>' not in height and re.search(r'\d+', height):
if tname in ('table', 'td', 'tr'):
pass
elif tname == 'img':
tattr['height'] = height
else:
styles.append('margin-top: %s' % self.ensure_unit(height))
if 'width' in tattr.keys():
width = tattr.pop('width').strip()
if width and re.search(r'\d+', width):
if tname in ('table', 'td', 'tr'):
pass
elif tname == 'img':
tattr['width'] = width
else:
styles.append('text-indent: %s' % self.ensure_unit(width))
if width.startswith('-'):
styles.append('margin-left: %s' % self.ensure_unit(width[1:]))
if 'bgcolor' in tattr.keys():
# no proprietary html allowed
if tname == 'div':
del tattr['bgcolor']
elif tname == 'font':
# Change font tags to span tags
tname = 'span'
if ttype in ('begin', 'single', 'single_ext'):
# move the face attribute to css font-family
if 'face' in tattr.keys():
face = tattr.pop('face').strip()
styles.append('font-family: "%s"' % face)
# Monitor the constantly changing font sizes, change them to ems and move
# them to css. The following will work for 'flat' font tags, but nested font tags
# will cause things to go wonky. Need to revert to the parent font tag's size
# when a closing tag is encountered.
if 'size' in tattr.keys():
sz = tattr.pop('size').strip().lower()
try:
float(sz)
except ValueError:
if sz in size_map.keys():
sz = size_map[sz]
else:
if sz.startswith('-') or sz.startswith('+'):
sz = self.current_font_size + float(sz)
if sz > 7:
sz = 7
elif sz < 1:
sz = 1
sz = str(int(sz))
styles.append('font-size: %s' % size_to_em_map[sz])
self.current_font_size = int(sz)
elif tname == 'img':
for attr in ('width', 'height'):
if attr in tattr:
val = tattr[attr]
if val.lower().endswith('em'):
try:
nval = float(val[:-2])
nval *= 16 * (168.451/72) # Assume this was set using the Kindle profile
tattr[attr] = "%dpx"%int(nval)
except:
del tattr[attr]
elif val.lower().endswith('%'):
del tattr[attr]
# convert the anchor tags
if 'filepos-id' in tattr:
tattr['id'] = tattr.pop('filepos-id')
if 'name' in tattr and tattr['name'] != tattr['id']:
tattr['name'] = tattr['id']
if 'filepos' in tattr:
filepos = tattr.pop('filepos')
try:
tattr['href'] = "#filepos%d" % int(filepos)
except ValueError:
pass
if styles:
ncls = None
rule = '; '.join(styles)
for sel, srule in self.tag_css_rules.items():
if srule == rule:
ncls = sel
break
if ncls is None:
self.tag_css_rule_cnt += 1
ncls = 'rule_%d' % self.tag_css_rule_cnt
self.tag_css_rules[ncls] = rule
cls = tattr.get('class', '')
cls = cls + (' ' if cls else '') + ncls
tattr['class'] = cls
# convert updated tag back to string representation
if len(tattr) == 0:
tattr = None
taginfo = (ttype, tname, tattr)
return self.taginfo_tostring(taginfo)
''' main only left in for testing outside of plugin '''
def main(argv=sys.argv):
if len(argv) != 2:
return 1
else:
infile = argv[1]
try:
print('Converting Mobi Markup Language to XHTML')
mlc = MobiMLConverter(infile)
print('Processing ...')
htmlstr, css, cssname = mlc.processml()
outname = infile.rsplit('.',1)[0] + '_converted.html'
file(outname, 'wb').write(htmlstr)
file(cssname, 'wb').write(css)
print('Completed')
print('XHTML version of book can be found at: ', outname)
except ValueError as e:
print("Error: %s" % e)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

40
Lector.pro Normal file
View File

@@ -0,0 +1,40 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
SOURCES += lector/__main__.py \
lector/annotations.py \
lector/contentwidgets.py \
lector/definitionsdialog.py \
lector/dockwidgets.py \
lector/library.py \
lector/metadatadialog.py \
lector/models.py \
lector/settingsdialog.py \
lector/toolbars.py \
lector/widgets.py \
lector/resources/definitions.py \
lector/resources/settingswindow.py \
lector/resources/metadata.py \
lector/resources/mainwindow.py
TRANSLATIONS += lector/resources/translations/Lector_cs.ts \
lector/resources/translations/Lector_es.ts \
lector/resources/translations/Lector_fr.ts \
lector/resources/translations/Lector_de.ts \
lector/resources/translations/Lector_zh.ts \
lector/resources/translations/Lector_ja.ts \
lector/resources/translations/Lector_pt.ts \
lector/resources/translations/SAMPLE.ts

View File

@@ -1,27 +1,47 @@
# Lector <p align="center"><img src="lector/resources/raw/logo/logotype_horizontal.png" alt="Lector" height="90px"></p>
Qt based ebook reader Qt based ebook reader
Currently supports: Currently supports:
* pdf * pdf
* epub * epub
* djvu
* fb2
* mobi * mobi
* azw / azw3 / azw4 * azw / azw3 / azw4
* cbr / cbz * cbr / cbz
* md
Support for a bunch of other formats is coming. Please see the TODO for additional information. ## Contribute
[Paypal](https://www.paypal.me/supportlector)
Bitcoin: 17jaxj26vFJNqQ2hEVerbBV5fpTusfqFro
## Requirements ## Requirements
### Needed
| Package | Version tested | | Package | Version tested |
| --- | --- | | --- | --- |
| Qt5 | 5.10.1 |
| Python | 3.6 | | Python | 3.6 |
| PyQt5 | 5.10.1 | | PyQt5 | 5.10.1 |
| python-requests | 2.18.4 | | python-lxml | 4.3.0 |
| python-beautifulsoup4 | 4.6.0 | | python-beautifulsoup4 | 4.6.0 |
| poppler-qt5 | 0.61.1 | | python-xmltodict | 0.11.0 |
| python-poppler-qt5 | 0.24.2 |
poppler-qt5 and python-poppler-qt5 are optional. ### Optional
| Package | Version tested | Required for |
| --- | --- | --- |
| python-pymupdf | 1.14.5 | PDF support |
| python-djvulibre | 0.8.4 | DjVu support |
| python-markdown | 3.0.1 | Markdown support |
| textile | 3.0.4 | TXT support |
## Support
When reporting issues:
* Make sure you're at the latest commit.
* Run with `$EXECUTABLEPATH debug`.
* Include the log `~/.local/share/Lector/Lector.log` AND terminal output.
* If you're having trouble with a book while the rest of the application / other books work, please link to a copy of the book itself.
* If nothing is working, please make sure the requirements mentioned above are all installed, and are at least at the version mentioned.
## Installation ## Installation
### Manual ### Manual
@@ -34,39 +54,55 @@ poppler-qt5 and python-poppler-qt5 are optional.
3. OR launch with `lector/__main__.py` 3. OR launch with `lector/__main__.py`
### Available packages ### Available packages
* [AUR](https://aur.archlinux.org/packages/lector-git/) * [AUR - Releases](https://aur.archlinux.org/packages/lector/)
* [AUR - Git](https://aur.archlinux.org/packages/lector-git/)
* [Gentoo (unofficial)](https://bitbucket.org/szymonsz/gen2-overlay/src/master/app-text/lector/)
* [Fedora (unofficial)](https://copr.fedorainfracloud.org/coprs/bugzy/lector/)
* [openSUSE](https://software.opensuse.org/package/lector)
## Reporting issues ## Translations
When reporting issues: 1. There is a `SAMPLE.ts` file [here](https://github.com/BasioMeusPuga/Lector/tree/master/lector/resources/translations). Open it in `Qt Linguist`.
2. Pick the language you wish to translate to.
3. Translate relevant strings.
4. Try to resist the urge to include profanity.
5. Save the file as `Lector_<language>` and send it to me, preferably as a pull request.
* If you're having trouble with a book while the rest of the application / other books work, please link to a copy of the book itself. Please keep the translations short. There's only so much space for UI elements.
* If nothing is working, please make sure the requirements mentioned above are all installed, and are at least at the version mentioned.
## Screenshots ## Screenshots
### Main window ### Main window
![alt tag](https://i.imgur.com/yrv2c0a.png) ![alt tag](https://i.imgur.com/516hRkS.png)
### Table view ### Table view
![alt tag](https://i.imgur.com/b1XdXqP.png) ![alt tag](https://i.imgur.com/o9An7AR.png)
### Book reading view ### Book reading view
![alt tag](https://i.imgur.com/Tei6TqF.png) ![alt tag](https://i.imgur.com/ITG63Fc.png)
### Distraction free view
![alt tag](https://i.imgur.com/g8Ltupy.png)
### Annotation support
![alt tag](https://i.imgur.com/gLK29F4.png)
### Comic reading view ### Comic reading view
![alt tag](https://i.imgur.com/U5JR35g.png) ![alt tag](https://i.imgur.com/rvvTQCM.png)
### Bookmark support ### Bookmark support
![alt tag](https://i.imgur.com/RZkmCzG.png) ![alt tag](https://i.imgur.com/Y7qoU8m.png)
### View profiles ### View profiles
![alt tag](https://i.imgur.com/gkJ88pi.png) ![alt tag](https://i.imgur.com/awE2q2K.png)
### Metadata editor ### Metadata editor
![alt tag](https://i.imgur.com/AqQREBf.png) ![alt tag](https://i.imgur.com/0CDpNO8.png)
### In program dictionary ### In program dictionary
![alt tag](https://i.imgur.com/Vh9xQUC.png) ![alt tag](https://i.imgur.com/RF72m2h.png)
### Settings window
![alt tag](https://i.imgur.com/l6zJXaH.png)
## Attributions ## Attributions
* [KindleUnpack](https://github.com/kevinhendricks/KindleUnpack) * [KindleUnpack](https://github.com/kevinhendricks/KindleUnpack)

82
TODO
View File

@@ -1,4 +1,10 @@
TODO TODO
General:
✓ Internationalization
✓ Application icon
✓ .desktop file
✓ Shift to logging instead of print statements
Flatpak and AppImage support
Options: Options:
✓ Automatic library management ✓ Automatic library management
✓ Recursive file addition ✓ Recursive file addition
@@ -25,9 +31,15 @@ TODO
✓ Information dialog widget ✓ Information dialog widget
✓ Allow editing of database data through the UI + for Bookmarks ✓ Allow editing of database data through the UI + for Bookmarks
✓ Include (action) icons with the applications ✓ Include (action) icons with the applications
✓ Drag and drop support for the library
✓ Tab reordering
Additional Settings:
✓ Create covers for books without them - VERY SLOW
Set focus to newly added file Set focus to newly added file
Reading: Reading:
✓ Navbar
✓ Drop down for TOC ✓ Drop down for TOC
✓ Treeview navigation for TOC
✓ Override the keypress event of the textedit ✓ Override the keypress event of the textedit
✓ Use format* icons for toolbar buttons ✓ Use format* icons for toolbar buttons
✓ Implement book view settings with a(nother) toolbar ✓ Implement book view settings with a(nother) toolbar
@@ -51,48 +63,80 @@ TODO
✓ Cache next and previous images ✓ Cache next and previous images
✓ Set context menu for definitions and the like ✓ Set context menu for definitions and the like
✓ Paragraph indentation ✓ Paragraph indentation
Search document using QTextCursor? ✓ Comic view keyboard shortcuts
Comic view keyboard shortcuts Comic view context menu
✓ Image rotation
✓ Make the bookmark dock float over the reading area
✓ Spacebar should not cut off lines at the top
✓ Track open bookmark windows so they can be closed quickly at exit
✓ Search document using QTextCursor
✓ Double page / column view
✓ For comics
Caching is currently non functional
Annotations
✓ Text
✓ Disable buttons for annotations, search in images
Adjust key navigation according to viewport dimensions
Filetypes: Filetypes:
✓ pdf support ✓ pdf support
Parse TOC Parse TOC
✓ epub support ✓ epub support
✓ Homegrown solution please ✓ Homegrown solution please
✓ cbz, cbr support ✓ cbz, cbr support
✓ Keep font settings enabled but only for background color ✓ Keep font settings enabled but only for background color
✓ Double page view
✓ Manga mode
✓ mobi, azw support ✓ mobi, azw support
Limit the extra files produced by KindleUnpack Limit the extra files produced by KindleUnpack
Have them save to memory Have them save to memory
✓ fb2 support
✓ Images need to show up in their placeholders
✓ djvu support
✓ markdown support
Other: Other:
✓ Define every widget in code ✓ Define every widget in code
Bugs: Bugs:
If there are files open and the database is deleted, TypeErrors result Deselecting all directories in the settings dialog also filters out manually added books
Cover culling does not occur if some other tab has initial focus Bookmark name for a page that's not on the TOC and has nothing before
Slider position change might be acting up too Screen position still keeps jumping when inside a paragraph
Take metadata from the database when opening the file Better recursion needed for fb2 toc
Search results should ignore punctuation
Keep text size for annotations
Drag and drop is acting out
Search and annotation buttons become visible when font settings are hidden in comics
Secondary: Secondary:
Annotations Auto switch between flow and single page mode
Text to speech
Definitions dialog needs to respond to escape
Zoom slider for comics / library
Tab tooltip
Additional Settings:
Find definitions on Google
Disable progressbar - 20% book addition speed improvement
Disable cover loading when reading - Saves ~2M / book
Special formatting for each chapter's title
Signal end of chapter with some text
Graphical themes Graphical themes
Change focus rectangle dimensions Change focus rectangle dimensions
Tab reordering Universal Ctrl + Tab
Allow tabs to detach and form their own windows Allow tabs to detach and form their own windows
Goodreads API: Ratings, Read, Recommendations Goodreads API: Ratings, Read, Recommendations
Get ISBN using python-isbnlib Get ISBN using python-isbnlib
Pagination
Use embedded fonts + CSS Use embedded fonts + CSS
Scrolling: Smooth / By Line txt, doc, chm support
Spacebar should not cut off lines at the top
Shift to logging instead of print statements
txt, doc, chm, djvu, fb2 support
Include icons for filetype emblems Include icons for filetype emblems
Drag and drop support for the library
Comic view modes Comic view modes
Continuous paging Continuous paging
Double pages
Leave comic images on disk in case tab isn't closed and files are remembered
Give the comic view a 'Save image as...' option
Ignore a / the / numbers for sorting purposes Ignore a / the / numbers for sorting purposes
? Add only one file type if multiple are present ? Add only one file type if multiple are present
? Plugin system for parsers
? Create emblem per filetype ? Create emblem per filetype
In application notifications
Notification in case the filter is filtering out all files with no option in place
Option to fit images to viewport
Need help with:
Double page view for books
Scrolling: Smooth / By Line
Annotation preview in listView
Pagination

View File

@@ -0,0 +1,143 @@
{
"app-id":"com.basiomeuspuga.Lector",
"runtime":"org.kde.Platform",
"runtime-version":"5.12",
"sdk":"org.kde.Sdk",
"command":"lector",
"rename-icon":"Lector",
"rename-desktop-file":"lector.desktop",
"rename-appdata-file":"lector.appdata.xml",
"finish-args":[
"--filesystem=host",
"--socket=x11",
"--socket=wayland",
"--device=dri",
"--share=ipc",
"--share=network"
],
"build-options":{
"cflags":"-O2",
"cxxflags":"-O2"
},
"modules":[
{
"name": "PyQt5",
"buildsystem": "simple",
"build-commands": [
"pip3 install --prefix=/app PyQt5-5.12-5.12.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl"
],
"modules":[
{
"name":"PyQt5-sip",
"sources":[
{
"type":"file",
"url":"https://files.pythonhosted.org/packages/ae/9c/74fba0b62a0756d214f9aded5b0184130f7866def7532fa68823f34feefa/PyQt5_sip-4.19.14-cp37-cp37m-manylinux1_x86_64.whl",
"sha256":"04bd0bb8b6f8fa03c2dfbdfff0c8c9bfb3f46a21dd4cac73983dae93bf949523"
}
],
"buildsystem":"simple",
"build-commands":[
"pip3 install --prefix=/app PyQt5_sip-4.19.14-cp37-cp37m-manylinux1_x86_64.whl"
]
}
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/5e/91/9ac8827d0af428e756f461a3aa7bcbc53d6450edfe026e27569f5ff3689e/PyQt5-5.12-5.12.1-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl",
"sha256": "fd5946795b39922f971cf92dec799aadc7544b7fa993a79b9f6059f13d817e6e"
}
]
},
{
"name":"beautifulsoup4",
"buildsystem":"simple",
"sources":[
{
"type":"file",
"url":"https://files.pythonhosted.org/packages/1d/5d/3260694a59df0ec52f8b4883f5d23b130bc237602a1411fa670eae12351e/beautifulsoup4-4.7.1-py3-none-any.whl",
"sha256":"034740f6cb549b4e932ae1ab975581e6103ac8f942200a0e9759065984391858"
}
],
"modules":[
{
"name": "soupsieve",
"sources":[
{
"type":"file",
"url":"https://files.pythonhosted.org/packages/bf/b3/2473abf05c4950c6a829ed5dcbc40d8b56d4351d15d6939c8ffb7c6b1a14/soupsieve-1.7.3-py2.py3-none-any.whl",
"sha256":"466910df7561796a60748826781ebe9a888f7a1668a636ae86783f44d10aae73"
}
],
"buildsystem":"simple",
"build-commands":[
"pip3 install --prefix=/app soupsieve-1.7.3-py2.py3-none-any.whl"
]
}
],
"build-commands":[
"pip3 install --prefix=/app beautifulsoup4-4.7.1-py3-none-any.whl"
]
},
{
"name":"xmltodict",
"buildsystem":"simple",
"sources":[
{
"type": "file",
"url":"https://files.pythonhosted.org/packages/28/fd/30d5c1d3ac29ce229f6bdc40bbc20b28f716e8b363140c26eff19122d8a5/xmltodict-0.12.0-py2.py3-none-any.whl",
"sha256":"8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"
}
],
"build-commands":[
"pip3 install --prefix=/app xmltodict-0.12.0-py2.py3-none-any.whl"
]
},
{
"name":"PyMuPDF",
"buildsystem":"simple",
"build-commands": [
"pip3 install --prefix=/app PyMuPDF-1.14.8-cp37-cp37m-manylinux1_x86_64.whl"
],
"sources":[
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/3c/df/4bfaee2631b505d502c2ba64aa437799f0a64125edb1d4c4c38044ad1ecc/PyMuPDF-1.14.8-cp37-cp37m-manylinux1_x86_64.whl",
"sha256": "a49798b58cce00e09b8a4431a5f64a400b11a0959f29507187c471208ce040a5"
}
]
},
{
"name":"lxml",
"buildsystem":"simple",
"sources":[
{
"type":"file",
"url":"https://files.pythonhosted.org/packages/08/f2/04bf04e42c070f65b64dbde02d2c94851251f19f5e9f803cc8f8bc61ac77/lxml-4.3.1-cp37-cp37m-manylinux1_x86_64.whl",
"sha256":"c0a7751ba1a4bfbe7831920d98cee3ce748007eab8dfda74593d44079568219a"
}
],
"build-commands":[
"pip3 install --prefix=/app lxml-4.3.1-cp37-cp37m-manylinux1_x86_64.whl"
]
},
{
"name":"lector",
"buildsystem":"simple",
"ensure-writable":[
"/lib/python*/site-packages/easy-install.pth"
],
"sources":[
{
"type":"git",
"url":"https://github.com/BasioMeusPuga/Lector.git"
}
],
"build-commands":[
"python3 setup.py build",
"python3 setup.py install --prefix=/app"
]
}
]
}

View File

@@ -1,317 +0,0 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import zipfile
from urllib.parse import unquote
from bs4 import BeautifulSoup
class EPUB:
def __init__(self, filename):
self.filename = filename
self.zip_file = None
self.book = {}
self.book['split_chapters'] = {}
def read_epub(self):
# This is the function that should error out in
# case the module cannot process the file
self.load_zip()
contents_path = self.get_file_path(
None, True)
if not contents_path:
return False # No opf was found so processing cannot continue
self.generate_book_metadata(contents_path)
self.parse_toc()
return True
def load_zip(self):
try:
self.zip_file = zipfile.ZipFile(
self.filename, mode='r', allowZip64=True)
except (KeyError, AttributeError, zipfile.BadZipFile):
print('Cannot parse ' + self.filename)
return
def parse_xml(self, filename, parser):
try:
this_xml = self.zip_file.read(filename).decode()
except KeyError:
short_filename = os.path.basename(self.filename)
print(f'{str(filename)} not found in {short_filename}')
return
root = BeautifulSoup(this_xml, parser)
return root
def get_file_path(self, filename, is_content_file=False):
# Use this to get the location of the content.opf file
# And maybe some other file that has a more well formatted
# We're going to all this trouble because there really is
# no going forward without a toc
if is_content_file:
container_location = self.get_file_path('container.xml')
xml = self.parse_xml(container_location, 'xml')
if xml:
root_item = xml.find('rootfile')
return root_item.get('full-path')
else:
possible_filenames = ('content.opf', 'package.opf')
for i in possible_filenames:
presumptive_location = self.get_file_path(i)
if presumptive_location:
return presumptive_location
for i in self.zip_file.filelist:
if os.path.basename(i.filename) == os.path.basename(filename):
return i.filename
return None
def read_from_zip(self, filename):
filename = unquote(filename)
try:
file_data = self.zip_file.read(filename)
return file_data
except KeyError:
file_path_actual = self.get_file_path(filename)
if file_path_actual:
return self.zip_file.read(file_path_actual)
else:
print('ePub module can\'t find ' + filename)
#______________________________________________________
def generate_book_metadata(self, contents_path):
self.book['title'] = 'Unknown'
self.book['author'] = 'Unknown'
self.book['isbn'] = None
self.book['tags'] = None
self.book['cover'] = None
self.book['toc_file'] = 'toc.ncx' # Overwritten if another one exists
# Parse XML
xml = self.parse_xml(contents_path, 'xml')
# Parse metadata
item_dict = {
'title': 'title',
'author': 'creator',
'year': 'date'}
for i in item_dict.items():
item = xml.find(i[1])
if item:
self.book[i[0]] = item.text
try:
self.book['year'] = int(self.book['year'][:4])
except (TypeError, KeyError, IndexError, ValueError):
self.book['year'] = 9999
# Get identifier
identifier_items = xml.find_all('identifier')
for i in identifier_items:
scheme = i.get('scheme')
try:
if scheme.lower() == 'isbn':
self.book['isbn'] = i.text
except AttributeError:
self.book['isbn'] = None
# Tags
tag_items = xml.find_all('subject')
tag_list = [i.text for i in tag_items]
self.book['tags'] = tag_list
# Get items
self.book['content_dict'] = {}
all_items = xml.find_all('item')
for i in all_items:
media_type = i.get('media-type')
this_id = i.get('id')
if media_type == 'application/xhtml+xml' or media_type == 'text/html':
self.book['content_dict'][this_id] = i.get('href')
if media_type == 'application/x-dtbncx+xml':
self.book['toc_file'] = i.get('href')
# Cover image
if 'cover' in this_id and media_type.split('/')[0] == 'image':
cover_href = i.get('href')
try:
self.book['cover'] = self.zip_file.read(cover_href)
except KeyError:
# The cover cannot be found according to the
# path specified in the content reference
self.book['cover'] = self.zip_file.read(
self.get_file_path(cover_href))
if not self.book['cover']:
# If no cover is located the conventional way,
# we go looking for the largest image in the book
biggest_image_size = 0
biggest_image = None
for j in self.zip_file.filelist:
if os.path.splitext(j.filename)[1] in ['.jpg', '.jpeg', '.png', '.gif']:
if j.file_size > biggest_image_size:
biggest_image = j.filename
biggest_image_size = j.file_size
if biggest_image:
self.book['cover'] = self.read_from_zip(biggest_image)
else:
print('No cover found for: ' + self.filename)
# Parse spine and arrange chapter paths acquired from the opf
# according to the order IN THE SPINE
spine_items = xml.find_all('itemref')
spine_order = []
for i in spine_items:
spine_order.append(i.get('idref'))
self.book['chapters_in_order'] = []
for i in spine_order:
chapter_path = self.book['content_dict'][i]
self.book['chapters_in_order'].append(chapter_path)
def parse_toc(self):
# This has no bearing on the actual order
# We're just using this to get chapter names
self.book['navpoint_dict'] = {}
toc_file = self.book['toc_file']
if toc_file:
toc_file = self.get_file_path(toc_file)
xml = self.parse_xml(toc_file, 'xml')
if not xml:
return
navpoints = xml.find_all('navPoint')
for i in navpoints:
chapter_title = i.find('text').text
chapter_source = i.find('content').get('src')
chapter_source_file = unquote(chapter_source.split('#')[0])
if '#' in chapter_source:
try:
self.book['split_chapters'][chapter_source_file].append(
(chapter_source.split('#')[1], chapter_title))
except KeyError:
self.book['split_chapters'][chapter_source_file] = []
self.book['split_chapters'][chapter_source_file].append(
(chapter_source.split('#')[1], chapter_title))
self.book['navpoint_dict'][chapter_source_file] = chapter_title
def parse_chapters(self, temp_dir=None, split_large_xml=False):
no_title_chapter = 0
self.book['book_list'] = []
for i in self.book['chapters_in_order']:
chapter_data = self.read_from_zip(i).decode()
if i in self.book['split_chapters'] and not split_large_xml:
split_chapters = get_split_content(
chapter_data, self.book['split_chapters'][i])
self.book['book_list'].extend(split_chapters)
elif split_large_xml:
# https://stackoverflow.com/questions/14444732/how-to-split-a-html-page-to-multiple-pages-using-python-and-beautiful-soup
markup = BeautifulSoup(chapter_data, 'xml')
chapters = []
pagebreaks = markup.find_all('pagebreak')
def next_element(elem):
while elem is not None:
elem = elem.next_sibling
if hasattr(elem, 'name'):
return elem
for pbreak in pagebreaks:
chapter = [str(pbreak)]
elem = next_element(pbreak)
while elem and elem.name != 'pagebreak':
chapter.append(str(elem))
elem = next_element(elem)
chapters.append('\n'.join(chapter))
for this_chapter in chapters:
fallback_title = str(no_title_chapter)
self.book['book_list'].append(
(fallback_title, this_chapter + ('<br/>' * 8)))
no_title_chapter += 1
else:
try:
self.book['book_list'].append(
(self.book['navpoint_dict'][i], chapter_data + ('<br/>' * 8)))
except KeyError:
fallback_title = str(no_title_chapter)
self.book['book_list'].append(
(fallback_title, chapter_data))
no_title_chapter += 1
cover_path = os.path.join(temp_dir, os.path.basename(self.filename)) + '- cover'
if self.book['cover']:
with open(cover_path, 'wb') as cover_temp:
cover_temp.write(self.book['cover'])
self.book['book_list'][0] = (
'Cover', f'<center><img src="{cover_path}" alt="Cover"></center>')
def get_split_content(chapter_data, split_by):
split_anchors = [i[0] for i in split_by]
chapter_titles = [i[1] for i in split_by]
return_list = []
xml = BeautifulSoup(chapter_data, 'lxml')
xml_string = xml.body.prettify()
for count, i in enumerate(split_anchors):
this_split = xml_string.split(i)
current_chapter = this_split[0]
bs_obj = BeautifulSoup(current_chapter, 'lxml')
# Since tags correspond to data following them, the first
# chunk will be ignored
# As will all empty chapters
if bs_obj.text == '\n' or bs_obj.text == '' or count == 0:
continue
bs_obj_string = str(bs_obj).replace('"&gt;', '', 1) + ('<br/>' * 8)
return_list.append(
(chapter_titles[count - 1], bs_obj_string))
xml_string = this_split[1]
bs_obj = BeautifulSoup(xml_string, 'lxml')
bs_obj_string = str(bs_obj).replace('"&gt;', '', 1) + ('<br/>' * 8)
return_list.append(
(chapter_titles[-1], bs_obj_string))
return return_list

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi
import os import os
__path__ = ["lib", os.path.dirname(__file__), "kindleunpack"] __path__ = ["lib", os.path.dirname(os.path.realpath(__file__)), "kindleunpack"]
import sys import sys
import codecs import codecs
@@ -140,6 +140,8 @@ if PY2:
# 0.76 pre-release version only fix name related issues in opf by not using original file name in mobi7 # 0.76 pre-release version only fix name related issues in opf by not using original file name in mobi7
# 0.77 bug fix for unpacking HDImages with included Fonts # 0.77 bug fix for unpacking HDImages with included Fonts
# 0.80 converted to work with both python 2.7 and Python 3.3 and later # 0.80 converted to work with both python 2.7 and Python 3.3 and later
# 0.81 various fixes
# 0.82 Handle calibre-generated mobis that can have skeletons with no fragments
DUMP = False DUMP = False
""" Set to True to dump all possible information. """ """ Set to True to dump all possible information. """
@@ -847,7 +849,7 @@ def process_all_mobi_headers(files, apnxfile, sect, mhlst, K8Boundary, k8only=Fa
return return
def unpackBook(infile, outdir, apnxfile=None, epubver='2', use_hd=True, dodump=False, dowriteraw=False, dosplitcombos=False): def unpackBook(infile, outdir, apnxfile=None, epubver='2', use_hd=False, dodump=False, dowriteraw=False, dosplitcombos=False):
global DUMP global DUMP
global WRITE_RAW_DATA global WRITE_RAW_DATA
global SPLIT_COMBO_MOBIS global SPLIT_COMBO_MOBIS
@@ -949,7 +951,7 @@ def main(argv=unicode_argv()):
global WRITE_RAW_DATA global WRITE_RAW_DATA
global SPLIT_COMBO_MOBIS global SPLIT_COMBO_MOBIS
print("KindleUnpack v0.80") print("KindleUnpack v0.82")
print(" Based on initial mobipocket version Copyright © 2009 Charles M. Hannum <root@ihack.net>") print(" Based on initial mobipocket version Copyright © 2009 Charles M. Hannum <root@ihack.net>")
print(" Extensive Extensions and Improvements Copyright © 2009-2014 ") print(" Extensive Extensions and Improvements Copyright © 2009-2014 ")
print(" by: P. Durrant, K. Hendricks, S. Siebert, fandrieu, DiapDealer, nickredding, tkeo.") print(" by: P. Durrant, K. Hendricks, S. Siebert, fandrieu, DiapDealer, nickredding, tkeo.")

View File

@@ -180,9 +180,11 @@ class K8Processor:
fragptr = 0 fragptr = 0
baseptr = 0 baseptr = 0
cnt = 0 cnt = 0
filename = 'part%04d.xhtml' % cnt
for [skelnum, skelname, fragcnt, skelpos, skellen] in self.skeltbl: for [skelnum, skelname, fragcnt, skelpos, skellen] in self.skeltbl:
baseptr = skelpos + skellen baseptr = skelpos + skellen
skeleton = text[skelpos: baseptr] skeleton = text[skelpos: baseptr]
aidtext = "0"
for i in range(fragcnt): for i in range(fragcnt):
[insertpos, idtext, filenum, seqnum, startpos, length] = self.fragtbl[fragptr] [insertpos, idtext, filenum, seqnum, startpos, length] = self.fragtbl[fragptr]
aidtext = idtext[12:-2] aidtext = idtext[12:-2]

View File

@@ -273,7 +273,7 @@ class OPFProcessor(object):
del metadata['CoverOffset'] del metadata['CoverOffset']
handleMetaPairs(data, metadata, 'Codec', 'output encoding') handleMetaPairs(data, metadata, 'Codec', 'output encoding')
# handle kindlegen specifc tags # handle kindlegen specific tags
handleTag(data, metadata, 'DictInLanguage', 'DictionaryInLanguage') handleTag(data, metadata, 'DictInLanguage', 'DictionaryInLanguage')
handleTag(data, metadata, 'DictOutLanguage', 'DictionaryOutLanguage') handleTag(data, metadata, 'DictOutLanguage', 'DictionaryOutLanguage')
handleMetaPairs(data, metadata, 'RegionMagnification', 'RegionMagnification') handleMetaPairs(data, metadata, 'RegionMagnification', 'RegionMagnification')

File diff suppressed because it is too large Load Diff

317
lector/annotations.py Normal file
View File

@@ -0,0 +1,317 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtCore, QtGui
from lector.resources import annotationswindow
logger = logging.getLogger(__name__)
class AnnotationsUI(QtWidgets.QDialog, annotationswindow.Ui_Dialog):
def __init__(self, parent=None):
super(AnnotationsUI, self).__init__()
self.setupUi(self)
self.parent = parent
self._translate = QtCore.QCoreApplication.translate
# Current annotation
self.modelIndex = None # The index of the annotations list model in the parent dialog
self.current_annotation = {}
# Populate annotation type
textmarkup_string = self._translate('AnnotationsUI', 'Text markup')
all_types = [textmarkup_string]
for i in all_types:
self.typeBox.addItem(i)
# Init defaults
self.default_stylesheet = self.foregroundCheck.styleSheet()
self.foregroundColor = QtGui.QColor.fromRgb(0, 0, 0)
self.underlineColor = QtGui.QColor.fromRgb(255, 0, 0)
self.highlightColor = QtGui.QColor.fromRgb(66, 209, 56)
self.underline_styles = {
'Solid': QtGui.QTextCharFormat.SingleUnderline,
'Dashes': QtGui.QTextCharFormat.DashUnderline,
'Dots': QtGui.QTextCharFormat.DotLine,
'Wavy': QtGui.QTextCharFormat.WaveUnderline}
# Push buttons
self.foregroundColorButton.clicked.connect(self.modify_annotation)
self.highlightColorButton.clicked.connect(self.modify_annotation)
self.underlineColorButton.clicked.connect(self.modify_annotation)
self.okButton.clicked.connect(self.ok_pressed)
self.cancelButton.clicked.connect(self.hide)
# Underline combo box
underline_items = ['Solid', 'Dashes', 'Dots', 'Wavy']
self.underlineType.addItems(underline_items)
self.underlineType.currentIndexChanged.connect(self.modify_annotation)
# Text markup related checkboxes
self.foregroundCheck.clicked.connect(self.modify_annotation)
self.highlightCheck.clicked.connect(self.modify_annotation)
self.boldCheck.clicked.connect(self.modify_annotation)
self.italicCheck.clicked.connect(self.modify_annotation)
self.underlineCheck.clicked.connect(self.modify_annotation)
def show_dialog(self, mode, index=None):
# TODO
# Account for annotation type here
# and point to a relevant set of widgets accordingly
if mode == 'edit' or mode == 'preview':
self.modelIndex = index
this_annotation = self.parent.annotationModel.data(
index, QtCore.Qt.UserRole)
annotation_name = this_annotation['name']
self.nameEdit.setText(annotation_name)
annotation_components = this_annotation['components']
if 'foregroundColor' in annotation_components:
self.foregroundCheck.setChecked(True)
self.foregroundColor = annotation_components['foregroundColor']
self.set_button_background_color(
self.foregroundColorButton, annotation_components['foregroundColor'])
else:
self.foregroundCheck.setChecked(False)
if 'highlightColor' in annotation_components:
self.highlightCheck.setChecked(True)
self.highlightColor = annotation_components['highlightColor']
self.set_button_background_color(
self.highlightColorButton, annotation_components['highlightColor'])
else:
self.highlightCheck.setChecked(False)
if 'bold' in annotation_components:
self.boldCheck.setChecked(True)
else:
self.boldCheck.setChecked(False)
if 'italic' in annotation_components:
self.italicCheck.setChecked(True)
else:
self.italicCheck.setChecked(False)
if 'underline' in annotation_components:
self.underlineCheck.setChecked(True)
underline_params = annotation_components['underline']
self.underlineType.setCurrentText(underline_params[0])
self.set_button_background_color(
self.underlineColorButton, underline_params[1])
else:
self.underlineCheck.setChecked(False)
elif mode == 'add':
new_annotation_string = self._translate('AnnotationsUI', 'New annotation')
self.nameEdit.setText(new_annotation_string)
all_checkboxes = (
self.foregroundCheck, self.highlightCheck,
self.boldCheck, self.italicCheck, self.underlineCheck)
for i in all_checkboxes:
i.setChecked(False)
self.modelIndex = None
self.set_button_background_color(
self.foregroundColorButton, self.foregroundColor)
self.set_button_background_color(
self.highlightColorButton, self.highlightColor)
self.set_button_background_color(
self.underlineColorButton, self.underlineColor)
self.update_preview()
if mode != 'preview':
self.show()
def set_button_background_color(self, button, color):
button.setStyleSheet(
"QPushButton {{background-color: {0}}}".format(color.name()))
def update_preview(self):
cursor = self.parent.previewView.textCursor()
cursor.setPosition(0)
cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor)
# TODO
# Other kinds of text markup
previewCharFormat = QtGui.QTextCharFormat()
if self.foregroundCheck.isChecked():
previewCharFormat.setForeground(self.foregroundColor)
highlight = QtCore.Qt.transparent
if self.highlightCheck.isChecked():
highlight = self.highlightColor
previewCharFormat.setBackground(highlight)
font_weight = QtGui.QFont.Normal
if self.boldCheck.isChecked():
font_weight = QtGui.QFont.Bold
previewCharFormat.setFontWeight(font_weight)
if self.italicCheck.isChecked():
previewCharFormat.setFontItalic(True)
if self.underlineCheck.isChecked():
previewCharFormat.setFontUnderline(True)
previewCharFormat.setUnderlineColor(self.underlineColor)
previewCharFormat.setUnderlineStyle(
self.underline_styles[self.underlineType.currentText()])
previewCharFormat.setFontStyleStrategy(
QtGui.QFont.PreferAntialias)
cursor.setCharFormat(previewCharFormat)
cursor.clearSelection()
self.parent.previewView.setTextCursor(cursor)
def modify_annotation(self):
sender = self.sender()
if isinstance(sender, QtWidgets.QCheckBox):
if not sender.isChecked():
self.update_preview()
return
new_color = None
if sender == self.foregroundColorButton:
new_color = self.get_color(self.foregroundColor)
self.foregroundColor = new_color
if sender == self.highlightColorButton:
new_color = self.get_color(self.highlightColor)
self.highlightColor = new_color
if sender == self.underlineColorButton:
new_color = self.get_color(self.underlineColor)
self.underlineColor = new_color
if new_color:
self.set_button_background_color(sender, new_color)
self.update_preview()
def get_color(self, current_color):
color_dialog = QtWidgets.QColorDialog()
new_color = color_dialog.getColor(current_color)
if new_color.isValid(): # Returned in case cancel is pressed
return new_color
else:
return current_color
def ok_pressed(self):
annotation_name = self.nameEdit.text()
if annotation_name == '':
self.nameEdit.setText('Why do you like bugs? WHY?')
return
annotation_components = {}
if self.foregroundCheck.isChecked():
annotation_components['foregroundColor'] = self.foregroundColor
if self.highlightCheck.isChecked():
annotation_components['highlightColor'] = self.highlightColor
if self.boldCheck.isChecked():
annotation_components['bold'] = True
if self.italicCheck.isChecked():
annotation_components['italic'] = True
if self.underlineCheck.isChecked():
annotation_components['underline'] = (
self.underlineType.currentText(), self.underlineColor)
self.current_annotation = {
'name': annotation_name,
'applicable_to': 'text',
'type': 'text_markup',
'components': annotation_components}
if self.modelIndex:
self.parent.annotationModel.setData(
self.modelIndex, annotation_name, QtCore.Qt.DisplayRole)
self.parent.annotationModel.setData(
self.modelIndex, self.current_annotation, QtCore.Qt.UserRole)
else: # New annotation
new_annotation_item = QtGui.QStandardItem()
new_annotation_item.setText(annotation_name)
new_annotation_item.setData(self.current_annotation, QtCore.Qt.UserRole)
self.parent.annotationModel.appendRow(new_annotation_item)
self.hide()
class AnnotationPlacement:
def __init__(self):
self.annotation_type = None
self.annotation_components = None
self.underline_styles = {
'Solid': QtGui.QTextCharFormat.SingleUnderline,
'Dashes': QtGui.QTextCharFormat.DashUnderline,
'Dots': QtGui.QTextCharFormat.DotLine,
'Wavy': QtGui.QTextCharFormat.WaveUnderline}
def set_current_annotation(self, annotation_type, annotation_components):
# Components expected to be a dictionary
self.annotation_type = annotation_type # This is currently unused
self.annotation_components = annotation_components
def format_text(self, cursor, start_here, end_here):
# This is applicable only to the PliantQTextBrowser
# for the text_markup style of annotation
# The cursor is the textCursor of the QTextEdit
# containing the text that has to be modified
if not self.annotation_components:
return
cursor.setPosition(start_here)
cursor.setPosition(end_here, QtGui.QTextCursor.KeepAnchor)
newCharFormat = QtGui.QTextCharFormat()
if 'foregroundColor' in self.annotation_components:
newCharFormat.setForeground(
self.annotation_components['foregroundColor'])
if 'highlightColor' in self.annotation_components:
newCharFormat.setBackground(
self.annotation_components['highlightColor'])
if 'bold' in self.annotation_components:
newCharFormat.setFontWeight(QtGui.QFont.Bold)
if 'italic' in self.annotation_components:
newCharFormat.setFontItalic(True)
if 'underline' in self.annotation_components:
newCharFormat.setFontUnderline(True)
newCharFormat.setUnderlineStyle(
self.underline_styles[self.annotation_components['underline'][0]])
newCharFormat.setUnderlineColor(
self.annotation_components['underline'][1])
newCharFormat.setFontStyleStrategy(
QtGui.QFont.PreferAntialias)
cursor.setCharFormat(newCharFormat)
cursor.clearSelection()
return cursor

1158
lector/contentwidgets.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader # This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga # Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -19,35 +17,80 @@
import os import os
import pickle import pickle
import sqlite3 import sqlite3
import logging
from PyQt5 import QtCore from PyQt5 import QtCore
logger = logging.getLogger(__name__)
class DatabaseInit: class DatabaseInit:
def __init__(self, location_prefix): def __init__(self, location_prefix):
os.makedirs(location_prefix, exist_ok=True) self.database_path = os.path.join(location_prefix, 'Lector.db')
database_path = os.path.join(location_prefix, 'Lector.db')
if not os.path.exists(database_path): self.books_table_columns = {
self.database = sqlite3.connect(database_path) 'id': 'INTEGER PRIMARY KEY',
'Title': 'TEXT',
'Author': 'TEXT',
'Year': 'INTEGER',
'DateAdded': 'BLOB',
'Path': 'TEXT',
'Position': 'BLOB',
'ISBN': 'TEXT',
'Tags': 'TEXT',
'Hash': 'TEXT',
'LastAccessed': 'BLOB',
'Bookmarks': 'BLOB',
'CoverImage': 'BLOB',
'Addition': 'TEXT',
'Annotations': 'BLOB'}
self.directories_table_columns = {
'id': 'INTEGER PRIMARY KEY',
'Path': 'TEXT',
'Name': 'TEXT',
'Tags': 'TEXT',
'CheckState': 'INTEGER'}
if os.path.exists(self.database_path):
self.check_columns()
else:
self.create_database() self.create_database()
def create_database(self): def create_database(self):
# TODO self.database = sqlite3.connect(self.database_path)
# Add separate columns for:
# addition mode column_string = ', '.join(
self.database.execute( [i[0] + ' ' + i[1] for i in self.books_table_columns.items()])
"CREATE TABLE books \ self.database.execute(f"CREATE TABLE books ({column_string})")
(id INTEGER PRIMARY KEY, Title TEXT, Author TEXT, Year INTEGER, DateAdded BLOB, \
Path TEXT, Position BLOB, ISBN TEXT, Tags TEXT, Hash TEXT, LastAccessed BLOB,\
Bookmarks BLOB, CoverImage BLOB)")
# CheckState is the standard QtCore.Qt.Checked / Unchecked # CheckState is the standard QtCore.Qt.Checked / Unchecked
self.database.execute( column_string = ', '.join(
"CREATE TABLE directories (id INTEGER PRIMARY KEY, Path TEXT, \ [i[0] + ' ' + i[1] for i in self.directories_table_columns.items()])
Name TEXT, Tags TEXT, CheckState INTEGER)") self.database.execute(f"CREATE TABLE directories ({column_string})")
self.database.commit() self.database.commit()
self.database.close() self.database.close()
def check_columns(self):
self.database = sqlite3.connect(self.database_path)
database_return = self.database.execute("PRAGMA table_info(books)").fetchall()
database_columns = [i[1] for i in database_return]
# This allows for addition of a column without having to reform the database
commit_required = False
for i in self.books_table_columns.items():
if i[0] not in database_columns:
commit_required = True
info_string = f'Database: Adding column "{i[0]}"'
logger.info(info_string)
sql_command = f"ALTER TABLE books ADD COLUMN {i[0]} {i[1]}"
self.database.execute(sql_command)
if commit_required:
self.database.commit()
class DatabaseFunctions: class DatabaseFunctions:
def __init__(self, location_prefix): def __init__(self, location_prefix):
@@ -55,10 +98,6 @@ class DatabaseFunctions:
self.database = sqlite3.connect(database_path) self.database = sqlite3.connect(database_path)
def set_library_paths(self, data_iterable): def set_library_paths(self, data_iterable):
# TODO
# INSERT OR REPLACE is not working
# So this is the old fashion kitchen sink approach
self.database.execute("DELETE FROM directories") self.database.execute("DELETE FROM directories")
for i in data_iterable: for i in data_iterable:
@@ -67,10 +106,13 @@ class DatabaseFunctions:
tags = i[2] tags = i[2]
is_checked = i[3] is_checked = i[3]
if not os.path.exists(path):
continue # Remove invalid paths from the database
sql_command = ( sql_command = (
"INSERT OR REPLACE INTO directories (ID, Path, Name, Tags, CheckState)\ "INSERT INTO directories (Path, Name, Tags, CheckState)\
VALUES ((SELECT ID FROM directories WHERE Path = ?), ?, ?, ?, ?)") VALUES (?, ?, ?, ?)")
self.database.execute(sql_command, [path, path, name, tags, is_checked]) self.database.execute(sql_command, [path, name, tags, is_checked])
self.database.commit() self.database.commit()
self.database.close() self.database.close()
@@ -95,6 +137,7 @@ class DatabaseFunctions:
path = i[1]['path'] path = i[1]['path']
cover = i[1]['cover_image'] cover = i[1]['cover_image']
isbn = i[1]['isbn'] isbn = i[1]['isbn']
addition_mode = i[1]['addition_mode']
tags = i[1]['tags'] tags = i[1]['tags']
if tags: if tags:
# Is a list. Needs to be a string # Is a list. Needs to be a string
@@ -105,8 +148,9 @@ class DatabaseFunctions:
sql_command_add = ( sql_command_add = (
"INSERT OR REPLACE INTO \ "INSERT OR REPLACE INTO \
books (Title, Author, Year, DateAdded, Path, ISBN, Tags, Hash, CoverImage) \ books (Title, Author, Year, DateAdded, Path, \
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)") ISBN, Tags, Hash, CoverImage, Addition) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
cover_insert = None cover_insert = None
if cover: if cover:
@@ -115,7 +159,8 @@ class DatabaseFunctions:
self.database.execute( self.database.execute(
sql_command_add, sql_command_add,
[title, author, year, current_datetime_bin, [title, author, year, current_datetime_bin,
path, isbn, tags, book_hash, cover_insert]) path, isbn, tags, book_hash, cover_insert,
addition_mode])
self.database.commit() self.database.commit()
self.database.close() self.database.close()
@@ -165,8 +210,9 @@ class DatabaseFunctions:
else: else:
return None return None
except (KeyError, sqlite3.OperationalError): except Exception as e:
print('SQLite is in wretched rebellion @ data fetching handling') error_string = 'SQLite is in wretched rebellion @ data fetching handling'
logger.critical(error_string + f' {type(e).__name__} Arguments: {e.args}')
def fetch_covers_only(self, hash_list): def fetch_covers_only(self, hash_list):
parameter_marks = ','.join(['?' for i in hash_list]) parameter_marks = ','.join(['?' for i in hash_list])
@@ -177,7 +223,7 @@ class DatabaseFunctions:
def modify_metadata(self, metadata_dict, book_hash): def modify_metadata(self, metadata_dict, book_hash):
def generate_binary(column, data): def generate_binary(column, data):
if column in ('Position', 'LastAccessed', 'Bookmarks'): if column in ('Position', 'LastAccessed', 'Bookmarks', 'Annotations'):
return sqlite3.Binary(pickle.dumps(data)) return sqlite3.Binary(pickle.dumps(data))
elif column == 'CoverImage': elif column == 'CoverImage':
return sqlite3.Binary(data) return sqlite3.Binary(data)
@@ -198,8 +244,9 @@ class DatabaseFunctions:
try: try:
self.database.execute( self.database.execute(
sql_command, update_data) sql_command, update_data)
except sqlite3.OperationalError: except sqlite3.OperationalError as e:
print('SQLite is in wretched rebellion @ metadata handling') error_string = 'SQLite is in wretched rebellion @ metadata handling'
logger.critical(error_string + f' {type(e).__name__} Arguments: {e.args}')
self.database.commit() self.database.commit()
self.database.close() self.database.close()
@@ -208,9 +255,10 @@ class DatabaseFunctions:
# target_data is an iterable # target_data is an iterable
if column_name == '*': if column_name == '*':
self.database.execute('DELETE FROM books') self.database.execute(
"DELETE FROM books WHERE NOT Addition = 'manual'")
else: else:
sql_command = f'DELETE FROM books WHERE {column_name} = ?' sql_command = f"DELETE FROM books WHERE {column_name} = ?"
for i in target_data: for i in target_data:
self.database.execute(sql_command, (i,)) self.database.execute(sql_command, (i,))

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader # This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga # Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -16,18 +14,33 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import requests import json
from PyQt5 import QtWidgets, QtCore, QtGui, QtMultimedia import logging
import urllib.request
from resources import definitions from PyQt5 import QtWidgets, QtCore, QtGui
logger = logging.getLogger(__name__)
try:
from PyQt5 import QtMultimedia
multimedia_available = True
except ImportError:
error_string = 'QtMultimedia not found. Sounds will not play.'
logger.error(error_string)
multimedia_available = False
from lector.resources import definitions
class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog): class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
def __init__(self, parent): def __init__(self, parent):
super(DefinitionsUI, self).__init__() super(DefinitionsUI, self).__init__()
self.setupUi(self) self.setupUi(self)
self._translate = QtCore.QCoreApplication.translate
self.parent = parent self.parent = parent
self.previous_position = None
self.setWindowFlags( self.setWindowFlags(
QtCore.Qt.Popup | QtCore.Qt.Popup |
@@ -36,8 +49,14 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
radius = 15 radius = 15
path = QtGui.QPainterPath() path = QtGui.QPainterPath()
path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius) path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius)
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask) try:
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask)
except TypeError: # Required for older versions of Qt
pass
self.definitionView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.app_id = 'bb7a91f9' self.app_id = 'bb7a91f9'
self.app_key = 'fefacdf6775c347b52e9efa2efe642ef' self.app_key = 'fefacdf6775c347b52e9efa2efe642ef'
@@ -48,25 +67,35 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
self.pronunciation_mp3 = None self.pronunciation_mp3 = None
self.okButton.clicked.connect(self.hide) self.okButton.clicked.connect(self.hide)
self.pronounceButton.clicked.connect(self.play_pronunciation) self.dialogBackground.clicked.connect(self.color_background)
if multimedia_available:
self.pronounceButton.clicked.connect(self.play_pronunciation)
else:
self.pronounceButton.setEnabled(False)
def api_call(self, url, word): def api_call(self, url, word):
language = self.parent.settings['dictionary_language'] language = self.parent.settings['dictionary_language']
url = url + language + '/' + word.lower() url = url + language + '/' + word.lower()
r = requests.get( req = urllib.request.Request(url)
url, req.add_header('app_id', self.app_id)
headers={'app_id': self.app_id, 'app_key': self.app_key}) req.add_header('app_key', self.app_key)
if r.status_code != 200: try:
print('A firm nope on the dictionary finding thing') response = urllib.request.urlopen(req)
if response.getcode() == 200:
return_json = json.loads(response.read())
return return_json
except Exception as e:
this_error = f'API access error'
logger.exception(this_error + f' {type(e).__name__} Arguments: {e.args}')
self.parent.display_error_notification(None)
return None return None
return r.json()
def find_definition(self, word): def find_definition(self, word):
word_root_json = self.api_call(self.root_url, word) word_root_json = self.api_call(self.root_url, word)
if not word_root_json: if not word_root_json:
logger.error('Word root json noped out: ' + word)
self.set_text(word, None, None, True) self.set_text(word, None, None, True)
return return
@@ -75,6 +104,8 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
definition_json = self.api_call(self.define_url, word_root) definition_json = self.api_call(self.define_url, word_root)
if not definition_json: if not definition_json:
logger.error('Definition json noped out: ' + word_root)
self.set_text(word, None, None, True)
return return
definitions = {} definitions = {}
@@ -92,7 +123,7 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
this_definition = j['definitions'][0].capitalize() this_definition = j['definitions'][0].capitalize()
except KeyError: except KeyError:
# The API also reports crossReferenceMarkers here # The API also reports crossReferenceMarkers here
pass this_definition = '<Not found>'
try: try:
definitions[category].add(this_definition) definitions[category].add(this_definition)
@@ -109,8 +140,9 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
html_string += f'<h2><em><strong>{word}</strong></em></h2>\n' html_string += f'<h2><em><strong>{word}</strong></em></h2>\n'
if nothing_found: if nothing_found:
nope_string = self._translate('DefinitionsUI', 'No definitions found in')
language = self.parent.settings['dictionary_language'].upper() language = self.parent.settings['dictionary_language'].upper()
html_string += f'<p><em>No definitions found in {language}<em></p>\n' html_string += f'<p><em>{nope_string} {language}<em></p>\n'
else: else:
# Word root # Word root
html_string += f'<p><em>Word root: <em>{word_root}</p>\n' html_string += f'<p><em>Word root: <em>{word_root}</p>\n'
@@ -133,18 +165,30 @@ class DefinitionsUI(QtWidgets.QDialog, definitions.Ui_Dialog):
background = self.parent.settings['dialog_background'] background = self.parent.settings['dialog_background']
else: else:
self.previous_position = self.pos() self.previous_position = self.pos()
background = self.parent.get_color() self.parent.get_color()
background = self.parent.settings['dialog_background']
# Calculate inverse color for the background so that
# the text doesn't look blank
r, g, b, alpha = background.getRgb()
inv_average = 255 - (r + g + b) // 3
if 100 < inv_average < 150:
inv_average = 255
foreground = QtGui.QColor(
inv_average, inv_average, inv_average, alpha)
self.setStyleSheet( self.setStyleSheet(
"QDialog {{background-color: {0}}}".format(background.name())) "QDialog {{background-color: {0}}}".format(background.name()))
self.definitionView.setStyleSheet( self.definitionView.setStyleSheet(
"QTextBrowser {{background-color: {0}}}".format(background.name())) "QTextBrowser {{color:{0}; background-color: {1}}}".format(
foreground.name(), background.name()))
if not set_initial: if not set_initial:
self.show() self.show()
def play_pronunciation(self): def play_pronunciation(self):
if not self.pronunciation_mp3: if not self.pronunciation_mp3 or not multimedia_available:
return return
media_content = QtMultimedia.QMediaContent( media_content = QtMultimedia.QMediaContent(

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader # This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga # Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -16,8 +14,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5 import QtWidgets, QtGui, QtCore
from resources import pie_chart
from lector.resources import pie_chart
logger = logging.getLogger(__name__)
class LibraryDelegate(QtWidgets.QStyledItemDelegate): class LibraryDelegate(QtWidgets.QStyledItemDelegate):
@@ -34,11 +37,7 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
option = option.__class__(option) option = option.__class__(option)
file_exists = index.data(QtCore.Qt.UserRole + 5) file_exists = index.data(QtCore.Qt.UserRole + 5)
metadata = index.data(QtCore.Qt.UserRole + 3) position_percent = index.data(QtCore.Qt.UserRole + 7)
position = metadata['position']
if position:
is_read = position['is_read']
# The shadow pixmap currently is set to 420 x 600 # The shadow pixmap currently is set to 420 x 600
# Only draw the cover shadow in case the setting is enabled # Only draw the cover shadow in case the setting is enabled
@@ -55,55 +54,20 @@ class LibraryDelegate(QtWidgets.QStyledItemDelegate):
if not file_exists: if not file_exists:
painter.setOpacity(.7) painter.setOpacity(.7)
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index) QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
read_icon = pie_chart.pixmapper(-1, None, None, 36) painter.setOpacity(1)
read_icon = pie_chart.pixmapper(
-1, None, self.parent.settings['consider_read_at'], 36)
x_draw = option.rect.bottomRight().x() - 30 x_draw = option.rect.bottomRight().x() - 30
y_draw = option.rect.bottomRight().y() - 35 y_draw = option.rect.bottomRight().y() - 35
painter.drawPixmap(x_draw, y_draw, read_icon) painter.drawPixmap(x_draw, y_draw, read_icon)
painter.setOpacity(1)
return return
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index) QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
if position:
if is_read:
current_chapter = total_chapters = 100
else:
try:
current_chapter = position['current_chapter']
total_chapters = position['total_chapters']
except KeyError:
return
if position_percent:
read_icon = pie_chart.pixmapper( read_icon = pie_chart.pixmapper(
current_chapter, total_chapters, self.temp_dir, 36) position_percent, self.temp_dir, self.parent.settings['consider_read_at'], 36)
x_draw = option.rect.bottomRight().x() - 30 x_draw = option.rect.bottomRight().x() - 30
y_draw = option.rect.bottomRight().y() - 35 y_draw = option.rect.bottomRight().y() - 35
if current_chapter != 1: painter.drawPixmap(x_draw, y_draw, read_icon)
painter.drawPixmap(x_draw, y_draw, read_icon)
class BookmarkDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None):
super(BookmarkDelegate, self).__init__(parent)
self.parent = parent
def sizeHint(self, *args):
dockwidget_width = self.parent.width() - 20
return QtCore.QSize(dockwidget_width, 50)
def paint(self, painter, option, index):
# TODO
# Alignment of the painted item
option = option.__class__(option)
chapter_index = index.data(QtCore.Qt.UserRole)
chapter_name = self.parent.window().bookToolBar.tocBox.itemText(chapter_index - 1)
if len(chapter_name) > 25:
chapter_name = chapter_name[:25] + '...'
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
painter.drawText(
option.rect,
QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight | QtCore.Qt.TextWordWrap,
' ' + chapter_name)

661
lector/dockwidgets.py Normal file
View File

@@ -0,0 +1,661 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import uuid
from PyQt5 import QtWidgets, QtGui, QtCore
from lector.models import BookmarkProxyModel
from lector.threaded import BackGroundTextSearch
class PliantDockWidget(QtWidgets.QDockWidget):
def __init__(self, main_window, notes_only, contentView, parent=None):
super(PliantDockWidget, self).__init__(parent)
self.main_window = main_window
self.notes_only = notes_only
self.contentView = contentView
self.current_annotation = None
self.parent = parent
# Models
# The following models belong to the sideDock
# bookmarkModel, bookmarkProxyModel
# annotationModel
# searchResultsModel
self.bookmarkModel = None
self.bookmarkProxyModel = None
self.annotationModel = None
self.searchResultsModel = None
# References
# All widgets belong to these
self.bookmarks = None
self.annotations = None
self.search = None
# Widgets
# Except this one
self.sideDockTabWidget = None
# Animate appearance
self.animation = QtCore.QPropertyAnimation(self, b'windowOpacity')
self.animation.setStartValue(0)
self.animation.setEndValue(1)
self.animation.setDuration(200)
def showEvent(self, event=None):
viewport_topRight = self.contentView.mapToGlobal(
self.contentView.viewport().rect().topRight())
desktop_size = QtWidgets.QDesktopWidget().screenGeometry()
dock_y = viewport_topRight.y()
dock_height = self.contentView.viewport().size().height()
if self.notes_only:
dock_width = dock_height = desktop_size.width() // 5.5
dock_x = QtGui.QCursor.pos().x()
dock_y = QtGui.QCursor.pos().y()
else:
dock_width = desktop_size.width() // 5
dock_x = viewport_topRight.x() - dock_width + 1
self.parent.navBar.hide()
self.main_window.active_docks.append(self)
self.setGeometry(dock_x, dock_y, dock_width, dock_height)
self.animation.start()
def hideEvent(self, event=None):
if self.notes_only:
annotationNoteEdit = self.findChild(QtWidgets.QTextEdit)
if self.current_annotation:
self.current_annotation['note'] = annotationNoteEdit.toPlainText()
try:
self.main_window.active_docks.remove(self)
except ValueError:
pass
def set_annotation(self, annotation):
self.current_annotation = annotation
def populate(self):
self.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable)
self.setTitleBarWidget(QtWidgets.QWidget(self)) # Removes titlebar
self.sideDockTabWidget = QtWidgets.QTabWidget(self)
self.setWidget(self.sideDockTabWidget)
# This order is important
self.bookmarkModel = QtGui.QStandardItemModel(self)
self.bookmarkProxyModel = BookmarkProxyModel(self)
self.bookmarks = Bookmarks(self)
self.bookmarks.generate_bookmark_model()
if not self.parent.are_we_doing_images_only:
self.annotationModel = QtGui.QStandardItemModel(self)
self.annotations = Annotations(self)
self.annotations.generate_annotation_model()
self.searchResultsModel = QtGui.QStandardItemModel(self)
self.search = Search(self)
def closeEvent(self, event):
self.hide()
# Ignoring this event prevents application closure
# when everything is fullscreened
event.ignore()
# For the following classes, the parent is the sideDock
# The parentTab is the parent... tab. So self.parent.parent
class Bookmarks:
def __init__(self, parent):
self.parent = parent
self.parentTab = self.parent.parent
self.bookmarkTreeView = QtWidgets.QTreeView(self.parent)
self._translate = QtCore.QCoreApplication.translate
self.bookmarks_string = self._translate('SideDock', 'Bookmarks')
self.bookmark_default = self._translate('SideDock', 'New bookmark')
self.create_widgets()
def create_widgets(self):
self.bookmarkTreeView.setHeaderHidden(True)
self.bookmarkTreeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.bookmarkTreeView.customContextMenuRequested.connect(
self.generate_bookmark_context_menu)
self.bookmarkTreeView.clicked.connect(self.navigate_to_bookmark)
# Add widget to side dock
self.parent.sideDockTabWidget.addTab(
self.bookmarkTreeView, self.bookmarks_string)
def add_bookmark(self, position=None):
identifier = uuid.uuid4().hex[:10]
if self.parentTab.are_we_doing_images_only:
chapter = self.parentTab.metadata['position']['current_chapter']
cursor_position = 0
else:
chapter, cursor_position = self.parent.contentView.record_position(True)
if position: # Should be the case when called from the context menu
cursor_position = position
self.parentTab.metadata['bookmarks'][identifier] = {
'chapter': chapter,
'cursor_position': cursor_position,
'description': self.bookmark_default}
self.parent.setVisible(True)
self.parent.sideDockTabWidget.setCurrentIndex(0)
self.add_bookmark_to_model(
self.bookmark_default, chapter, cursor_position, identifier, True)
def add_bookmark_to_model(
self, description, chapter_number, cursor_position,
identifier, new_bookmark=False):
def edit_new_bookmark(parent_item):
new_child = parent_item.child(parent_item.rowCount() - 1, 0)
source_index = self.parent.bookmarkModel.indexFromItem(new_child)
edit_index = self.bookmarkTreeView.model().mapFromSource(source_index)
self.parent.activateWindow()
self.bookmarkTreeView.setFocus()
self.bookmarkTreeView.setCurrentIndex(edit_index)
self.bookmarkTreeView.edit(edit_index)
def get_chapter_name(chapter_number):
for i in reversed(self.parentTab.metadata['toc']):
if i[2] <= chapter_number:
return i[1]
return 'Unknown'
bookmark = QtGui.QStandardItem()
bookmark.setData(False, QtCore.Qt.UserRole + 10) # Is Parent
bookmark.setData(chapter_number, QtCore.Qt.UserRole) # Chapter number
bookmark.setData(cursor_position, QtCore.Qt.UserRole + 1) # Cursor Position
bookmark.setData(identifier, QtCore.Qt.UserRole + 2) # Identifier
bookmark.setData(description, QtCore.Qt.DisplayRole) # Description
bookmark_chapter_name = get_chapter_name(chapter_number)
for i in range(self.parent.bookmarkModel.rowCount()):
parentIndex = self.parent.bookmarkModel.index(i, 0)
parent_chapter_number = parentIndex.data(QtCore.Qt.UserRole)
parent_chapter_name = parentIndex.data(QtCore.Qt.DisplayRole)
# This prevents duplication of the bookmark in the new
# navigation model
if ((parent_chapter_number <= chapter_number) and
(parent_chapter_name == bookmark_chapter_name)):
bookmarkParent = self.parent.bookmarkModel.itemFromIndex(parentIndex)
bookmarkParent.appendRow(bookmark)
if new_bookmark:
edit_new_bookmark(bookmarkParent)
return
# In case no parent item exists
bookmarkParent = QtGui.QStandardItem()
bookmarkParent.setData(True, QtCore.Qt.UserRole + 10) # Is Parent
bookmarkParent.setFlags(bookmarkParent.flags() & ~QtCore.Qt.ItemIsEditable) # Is Editable
bookmarkParent.setData(get_chapter_name(chapter_number), QtCore.Qt.DisplayRole)
bookmarkParent.setData(chapter_number, QtCore.Qt.UserRole)
bookmarkParent.appendRow(bookmark)
self.parent.bookmarkModel.appendRow(bookmarkParent)
if new_bookmark:
edit_new_bookmark(bookmarkParent)
def navigate_to_bookmark(self, index):
if not index.isValid():
return
is_parent = self.parent.bookmarkProxyModel.data(
index, QtCore.Qt.UserRole + 10)
if is_parent:
chapter_number = self.parent.bookmarkProxyModel.data(
index, QtCore.Qt.UserRole)
self.parentTab.set_content(chapter_number, True, True)
return
chapter = self.parent.bookmarkProxyModel.data(
index, QtCore.Qt.UserRole)
cursor_position = self.parent.bookmarkProxyModel.data(
index, QtCore.Qt.UserRole + 1)
self.parentTab.set_content(chapter, True, True)
if not self.parentTab.are_we_doing_images_only:
self.parentTab.set_cursor_position(cursor_position)
def generate_bookmark_model(self):
for i in self.parentTab.metadata['bookmarks'].items():
description = i[1]['description']
chapter = i[1]['chapter']
cursor_position = i[1]['cursor_position']
identifier = i[0]
self.add_bookmark_to_model(
description, chapter, cursor_position, identifier)
self.generate_bookmark_proxy_model()
def generate_bookmark_proxy_model(self):
self.parent.bookmarkProxyModel.setSourceModel(self.parent.bookmarkModel)
self.parent.bookmarkProxyModel.setSortCaseSensitivity(False)
self.parent.bookmarkProxyModel.setSortRole(QtCore.Qt.UserRole)
self.parent.bookmarkProxyModel.sort(0)
self.bookmarkTreeView.setModel(self.parent.bookmarkProxyModel)
def generate_bookmark_context_menu(self, position):
index = self.bookmarkTreeView.indexAt(position)
if not index.isValid():
return
is_parent = self.parent.bookmarkProxyModel.data(
index, QtCore.Qt.UserRole + 10)
if is_parent:
return
bookmarkMenu = QtWidgets.QMenu()
editAction = bookmarkMenu.addAction(
self.parentTab.main_window.QImageFactory.get_image('edit-rename'),
self._translate('Tab', 'Edit'))
deleteAction = bookmarkMenu.addAction(
self.parentTab.main_window.QImageFactory.get_image('trash-empty'),
self._translate('Tab', 'Delete'))
action = bookmarkMenu.exec_(
self.bookmarkTreeView.mapToGlobal(position))
if action == editAction:
self.bookmarkTreeView.edit(index)
if action == deleteAction:
child_index = self.parent.bookmarkProxyModel.mapToSource(index)
parent_index = child_index.parent()
child_rows = self.parent.bookmarkModel.itemFromIndex(
parent_index).rowCount()
delete_uuid = self.parent.bookmarkModel.data(
child_index, QtCore.Qt.UserRole + 2)
self.parentTab.metadata['bookmarks'].pop(delete_uuid)
self.parent.bookmarkModel.removeRow(
child_index.row(), child_index.parent())
if child_rows == 1:
self.parent.bookmarkModel.removeRow(parent_index.row())
class Annotations:
def __init__(self, parent):
self.parent = parent
self.parentTab = self.parent.parent
self.annotationListView = QtWidgets.QListView(self.parent)
self._translate = QtCore.QCoreApplication.translate
self.annotations_string = self._translate('SideDock', 'Annotations')
self.create_widgets()
def create_widgets(self):
self.annotationListView.setEditTriggers(QtWidgets.QListView.NoEditTriggers)
self.annotationListView.doubleClicked.connect(
self.parent.contentView.toggle_annotation_mode)
# Add widget to side dock
self.parent.sideDockTabWidget.addTab(
self.annotationListView, self.annotations_string)
def generate_annotation_model(self):
# TODO
# Annotation previews will require creation of a
# QStyledItemDelegate
saved_annotations = self.parent.main_window.settings['annotations']
if not saved_annotations:
return
# Create annotation model
for i in saved_annotations:
item = QtGui.QStandardItem()
item.setText(i['name'])
item.setData(i, QtCore.Qt.UserRole)
self.parent.annotationModel.appendRow(item)
self.annotationListView.setModel(self.parent.annotationModel)
class Search:
def __init__(self, parent):
self.parent = parent
self.parentTab = self.parent.parent
self.searchThread = BackGroundTextSearch()
self.searchOptionsLayout = QtWidgets.QHBoxLayout()
self.searchTabLayout = QtWidgets.QVBoxLayout()
self.searchTimer = QtCore.QTimer(self.parent)
self.searchLineEdit = QtWidgets.QLineEdit(self.parent)
self.searchBookButton = QtWidgets.QToolButton(self.parent)
self.caseSensitiveSearchButton = QtWidgets.QToolButton(self.parent)
self.matchWholeWordButton = QtWidgets.QToolButton(self.parent)
self.searchResultsTreeView = QtWidgets.QTreeView(self.parent)
self._translate = QtCore.QCoreApplication.translate
self.search_string = self._translate('SideDock', 'Search')
self.search_book_string = self._translate('SideDock', 'Search entire book')
self.case_sensitive_string = self._translate('SideDock', 'Match case')
self.match_word_string = self._translate('SideDock', 'Match word')
self.create_widgets()
def create_widgets(self):
self.searchThread.finished.connect(self.generate_search_result_model)
self.searchTimer.setSingleShot(True)
self.searchTimer.timeout.connect(self.set_search_options)
self.searchLineEdit.textChanged.connect(
lambda: self.searchLineEdit.setStyleSheet(
QtWidgets.QLineEdit.styleSheet(self.parent)))
self.searchLineEdit.textChanged.connect(
lambda: self.searchTimer.start(500))
self.searchBookButton.clicked.connect(
lambda: self.searchTimer.start(100))
self.caseSensitiveSearchButton.clicked.connect(
lambda: self.searchTimer.start(100))
self.matchWholeWordButton.clicked.connect(
lambda: self.searchTimer.start(100))
self.searchLineEdit.setFocusPolicy(QtCore.Qt.StrongFocus)
self.searchLineEdit.setClearButtonEnabled(True)
self.searchLineEdit.setPlaceholderText(self.search_string)
self.searchBookButton.setIcon(
self.parent.main_window.QImageFactory.get_image('view-readermode'))
self.searchBookButton.setToolTip(self.search_book_string)
self.searchBookButton.setCheckable(True)
self.searchBookButton.setAutoRaise(True)
self.searchBookButton.setIconSize(QtCore.QSize(20, 20))
self.caseSensitiveSearchButton.setIcon(
self.parent.main_window.QImageFactory.get_image('search-case'))
self.caseSensitiveSearchButton.setToolTip(self.case_sensitive_string)
self.caseSensitiveSearchButton.setCheckable(True)
self.caseSensitiveSearchButton.setAutoRaise(True)
self.caseSensitiveSearchButton.setIconSize(QtCore.QSize(20, 20))
self.matchWholeWordButton.setIcon(
self.parent.main_window.QImageFactory.get_image('search-word'))
self.matchWholeWordButton.setToolTip(self.match_word_string)
self.matchWholeWordButton.setCheckable(True)
self.matchWholeWordButton.setAutoRaise(True)
self.matchWholeWordButton.setIconSize(QtCore.QSize(20, 20))
self.searchOptionsLayout.setContentsMargins(0, 3, 0, 0)
self.searchOptionsLayout.addWidget(self.searchLineEdit)
self.searchOptionsLayout.addWidget(self.searchBookButton)
self.searchOptionsLayout.addWidget(self.caseSensitiveSearchButton)
self.searchOptionsLayout.addWidget(self.matchWholeWordButton)
self.searchResultsTreeView.setHeaderHidden(True)
self.searchResultsTreeView.setEditTriggers(
QtWidgets.QTreeView.NoEditTriggers)
self.searchResultsTreeView.clicked.connect(
self.navigate_to_search_result)
self.searchTabLayout.addLayout(self.searchOptionsLayout)
self.searchTabLayout.addWidget(self.searchResultsTreeView)
self.searchTabLayout.setContentsMargins(0, 0, 0, 0)
self.searchTabWidget = QtWidgets.QWidget(self.parent)
self.searchTabWidget.setLayout(self.searchTabLayout)
# Add widget to side dock
self.parent.sideDockTabWidget.addTab(
self.searchTabWidget, self.search_string)
def set_search_options(self):
def generate_title_content_pair(required_chapters):
title_content_list = []
for i in self.parentTab.metadata['toc']:
if i[2] in required_chapters:
title_content_list.append(
(i[1], self.parentTab.metadata['content'][i[2] - 1], i[2]))
return title_content_list
# Select either the current chapter or all chapters
# Function name is descriptive
chapter_numbers = (self.parentTab.metadata['position']['current_chapter'],)
if self.searchBookButton.isChecked():
chapter_numbers = [i + 1 for i in range(len(self.parentTab.metadata['content']))]
search_content = generate_title_content_pair(chapter_numbers)
self.searchThread.set_search_options(
search_content,
self.searchLineEdit.text(),
self.caseSensitiveSearchButton.isChecked(),
self.matchWholeWordButton.isChecked())
self.searchThread.start()
def generate_search_result_model(self):
self.parent.searchResultsModel.clear()
search_results = self.searchThread.search_results
for i in search_results:
parentItem = QtGui.QStandardItem()
parentItem.setData(True, QtCore.Qt.UserRole) # Is parent?
parentItem.setData(i, QtCore.Qt.UserRole + 3) # Display text for label
for j in search_results[i]:
childItem = QtGui.QStandardItem(parentItem)
childItem.setData(False, QtCore.Qt.UserRole) # Is parent?
childItem.setData(j[3], QtCore.Qt.UserRole + 1) # Chapter index
childItem.setData(j[0], QtCore.Qt.UserRole + 2) # Cursor Position
childItem.setData(j[1], QtCore.Qt.UserRole + 3) # Display text for label
childItem.setData(j[2], QtCore.Qt.UserRole + 4) # Search term
parentItem.appendRow(childItem)
self.parent.searchResultsModel.appendRow(parentItem)
self.searchResultsTreeView.setModel(self.parent.searchResultsModel)
self.searchResultsTreeView.expandToDepth(1)
# Reset stylesheet in case something is found
if search_results:
self.searchLineEdit.setStyleSheet(
QtWidgets.QLineEdit.styleSheet(self.parent))
# Or set to Red in case nothing is found
if not search_results and len(self.searchLineEdit.text()) > 2:
self.searchLineEdit.setStyleSheet("QLineEdit {color: red;}")
# We'll be putting in labels instead of making a delegate
# QLabels can understand RTF, and they also have the somewhat
# distinct advantage of being a lot less work than a delegate
def generate_label(index):
label_text = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 3)
labelWidget = PliantLabelWidget(index, self.navigate_to_search_result)
labelWidget.setText(label_text)
self.searchResultsTreeView.setIndexWidget(index, labelWidget)
for parent_iter in range(self.parent.searchResultsModel.rowCount()):
parentItem = self.parent.searchResultsModel.item(parent_iter)
parentIndex = self.parent.searchResultsModel.index(parent_iter, 0)
generate_label(parentIndex)
for child_iter in range(parentItem.rowCount()):
childIndex = self.parent.searchResultsModel.index(child_iter, 0, parentIndex)
generate_label(childIndex)
def navigate_to_search_result(self, index):
if not index.isValid():
return
is_parent = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole)
if is_parent:
return
chapter_number = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 1)
cursor_position = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 2)
search_term = self.parent.searchResultsModel.data(index, QtCore.Qt.UserRole + 4)
self.parentTab.set_content(chapter_number, True, True)
if not self.parentTab.are_we_doing_images_only:
self.parentTab.set_cursor_position(
cursor_position, len(search_term))
class PliantLabelWidget(QtWidgets.QLabel):
# This is a hack to get clickable / editable appearance
# search results in the tree view.
def __init__(self, index, navigate_to_search_result):
super(PliantLabelWidget, self).__init__()
self.index = index
self.navigate_to_search_result = navigate_to_search_result
def mousePressEvent(self, QMouseEvent):
self.navigate_to_search_result(self.index)
QtWidgets.QLabel.mousePressEvent(self, QMouseEvent)
class PliantNavBarWidget(QtWidgets.QDockWidget):
def __init__(self, main_window, contentView, parent):
super(PliantNavBarWidget, self).__init__(parent)
self.main_window = main_window
self.contentView = contentView
self.parent = parent
self.setWindowTitle('Navigation')
# Animate appearance
self.animation = QtCore.QPropertyAnimation(self, b'windowOpacity')
self.animation.setDuration(200)
self.animation.setStartValue(0)
self.animation.setEndValue(.8)
background = self.main_window.settings['dialog_background']
self.setStyleSheet(
"QDockWidget {{background-color: {0}}}".format(background.name()))
self.backButton = QtWidgets.QPushButton()
self.backButton.setFlat(True)
icon = QtGui.QIcon()
icon.addPixmap(
QtGui.QPixmap(":/images/previous.png"),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.backButton.setIcon(icon)
self.backButton.setIconSize(QtCore.QSize(24, 24))
self.nextButton = QtWidgets.QPushButton()
self.nextButton.setFlat(True)
icon = QtGui.QIcon()
icon.addPixmap(
QtGui.QPixmap(":/images/next.png"),
QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.nextButton.setIcon(icon)
self.nextButton.setIconSize(QtCore.QSize(24, 24))
self.backButton.clicked.connect(lambda: self.button_click(-1))
self.nextButton.clicked.connect(lambda: self.button_click(1))
self.tocComboBox = FixedComboBox(self)
self.populate_combo_box()
self.navLayout = QtWidgets.QHBoxLayout()
self.navLayout.addWidget(self.backButton)
self.navLayout.addWidget(self.tocComboBox)
self.navLayout.addWidget(self.nextButton)
self.navWidget = QtWidgets.QWidget()
self.navWidget.setLayout(self.navLayout)
self.setWidget(self.navWidget)
def showEvent(self, event=None):
# TODO
# See what happens when the size of the viewport is smaller
# than the size of the dock
viewport_bottomRight = self.contentView.mapToGlobal(
self.contentView.viewport().rect().bottomRight())
# Dock dimensions
desktop_size = QtWidgets.QDesktopWidget().screenGeometry()
dock_width = desktop_size.width() // 4.5
dock_height = 30
dock_x = viewport_bottomRight.x() - dock_width - 30
dock_y = viewport_bottomRight.y() - 70
self.main_window.active_docks.append(self)
self.setGeometry(dock_x, dock_y, dock_width, dock_height)
# Rounded
radius = 20
path = QtGui.QPainterPath()
path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius)
try:
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask)
except TypeError: # Required for older versions of Qt
pass
self.animation.start()
def populate_combo_box(self):
def set_toc_position(tocTree):
currentIndex = tocTree.currentIndex()
required_position = currentIndex.data(QtCore.Qt.UserRole)
self.return_focus()
self.parent.set_content(required_position, True, True)
# Create the Combobox / Treeview combination
tocTree = QtWidgets.QTreeView()
self.tocComboBox.setView(tocTree)
self.tocComboBox.setModel(self.parent.tocModel)
tocTree.setRootIsDecorated(False)
tocTree.setItemsExpandable(False)
tocTree.expandAll()
# Set the position of the QComboBox
self.parent.set_tocBox_index(None, self.tocComboBox)
# Make clicking do something
self.tocComboBox.currentIndexChanged.connect(
lambda: set_toc_position(tocTree))
def button_click(self, change):
self.contentView.common_functions.change_chapter(change)
self.return_focus()
def return_focus(self):
# The NavBar needs to be hidden after clicking
self.parent.activateWindow()
self.parent.contentView.setFocus()
self.parent.mouseHideTimer.start()
class FixedComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
super(FixedComboBox, self).__init__(parent)
screen_width = QtWidgets.QDesktopWidget().screenGeometry().width()
self.adjusted_size = screen_width // 6
def sizeHint(self):
return self.minimumSizeHint()
def minimumSizeHint(self):
return QtCore.QSize(self.adjusted_size, 32)
def wheelEvent(self, QWheelEvent):
# Disable mouse wheel scrolling in the ComboBox
return

View File

@@ -1,7 +1,5 @@
#!usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader # This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2018 BasioMeusPuga # Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -16,8 +14,15 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from PyQt5 import QtGui import logging
from resources import resources
from PyQt5 import QtCore, QtGui, QtWidgets
from lector import database
from lector.settings import Settings
from lector.resources import resources
logger = logging.getLogger(__name__)
class QImageFactory: class QImageFactory:
@@ -30,3 +35,317 @@ class QImageFactory:
this_qicon = QtGui.QIcon(icon_path) this_qicon = QtGui.QIcon(icon_path)
return this_qicon return this_qicon
# For nearly all cases below, code remains unchanged from its
# state in the __main__ module. References to objects have been
# made in the respective __init__ functions of the classes here
class CoverLoadingAndCulling:
def __init__(self, main_window):
self.main_window = main_window
self.lib_ref = self.main_window.lib_ref
self.listView = self.main_window.listView
self.database_path = self.main_window.database_path
def cull_covers(self, event=None):
blank_pixmap = QtGui.QPixmap()
blank_pixmap.load(':/images/blank.png') # Keep this. Removing it causes the
# listView to go blank on a resize
all_indexes = set()
for i in range(self.lib_ref.itemProxyModel.rowCount()):
all_indexes.add(self.lib_ref.itemProxyModel.index(i, 0))
y_range = list(range(0, self.listView.viewport().height(), 100))
y_range.extend((-20, self.listView.viewport().height() + 20))
x_range = range(0, self.listView.viewport().width(), 80)
visible_indexes = set()
for i in y_range:
for j in x_range:
this_index = self.listView.indexAt(QtCore.QPoint(j, i))
visible_indexes.add(this_index)
invisible_indexes = all_indexes - visible_indexes
for i in invisible_indexes:
model_index = self.lib_ref.itemProxyModel.mapToSource(i)
this_item = self.lib_ref.libraryModel.item(model_index.row())
if this_item:
this_item.setIcon(QtGui.QIcon(blank_pixmap))
this_item.setData(False, QtCore.Qt.UserRole + 8)
hash_index_dict = {}
hash_list = []
for i in visible_indexes:
model_index = self.lib_ref.itemProxyModel.mapToSource(i)
book_hash = self.lib_ref.libraryModel.data(
model_index, QtCore.Qt.UserRole + 6)
cover_displayed = self.lib_ref.libraryModel.data(
model_index, QtCore.Qt.UserRole + 8)
if book_hash and not cover_displayed:
hash_list.append(book_hash)
hash_index_dict[book_hash] = model_index
all_covers = database.DatabaseFunctions(
self.database_path).fetch_covers_only(hash_list)
for i in all_covers:
book_hash = i[0]
cover = i[1]
model_index = hash_index_dict[book_hash]
book_item = self.lib_ref.libraryModel.item(model_index.row())
self.cover_loader(book_item, cover)
def load_all_covers(self):
all_covers_db = database.DatabaseFunctions(
self.database_path).fetch_data(
('Hash', 'CoverImage',),
'books',
{'Hash': ''},
'LIKE')
if not all_covers_db:
return
all_covers = {
i[0]: i[1] for i in all_covers_db}
for i in range(self.lib_ref.libraryModel.rowCount()):
this_item = self.lib_ref.libraryModel.item(i, 0)
is_cover_already_displayed = this_item.data(QtCore.Qt.UserRole + 8)
if is_cover_already_displayed:
continue
book_hash = this_item.data(QtCore.Qt.UserRole + 6)
cover = all_covers[book_hash]
self.cover_loader(this_item, cover)
def cover_loader(self, item, cover):
img_pixmap = QtGui.QPixmap()
if cover:
img_pixmap.loadFromData(cover)
else:
img_pixmap.load(':/images/NotFound.png')
img_pixmap = img_pixmap.scaled(
420, 600, QtCore.Qt.IgnoreAspectRatio)
item.setIcon(QtGui.QIcon(img_pixmap))
item.setData(True, QtCore.Qt.UserRole + 8)
class ViewProfileModification:
def __init__(self, main_window):
self.main_window = main_window
self.listView = self.main_window.listView
self.settings = self.main_window.settings
self.bookToolBar = self.main_window.bookToolBar
self.comic_profile = self.main_window.comic_profile
self.tabWidget = self.main_window.tabWidget
self.alignment_dict = self.main_window.alignment_dict
def get_color(self, signal_sender):
def open_color_dialog(current_color):
color_dialog = QtWidgets.QColorDialog()
new_color = color_dialog.getColor(current_color)
if new_color.isValid(): # Returned in case cancel is pressed
return new_color
else:
return current_color
# Special cases that don't affect (comic)book display
if signal_sender == 'libraryBackground':
current_color = self.settings['listview_background']
new_color = open_color_dialog(current_color)
self.listView.setStyleSheet("QListView {{background-color: {0}}}".format(
new_color.name()))
self.settings['listview_background'] = new_color
return
if signal_sender == 'dialogBackground':
current_color = self.settings['dialog_background']
new_color = open_color_dialog(current_color)
self.settings['dialog_background'] = new_color
return
profile_index = self.bookToolBar.profileBox.currentIndex()
current_profile = self.bookToolBar.profileBox.itemData(
profile_index, QtCore.Qt.UserRole)
# Retain current values on opening a new dialog
if signal_sender == 'fgColor':
current_color = current_profile['foreground']
new_color = open_color_dialog(current_color)
self.bookToolBar.colorBoxFG.setStyleSheet(
'background-color: %s' % new_color.name())
current_profile['foreground'] = new_color
elif signal_sender == 'bgColor':
current_color = current_profile['background']
new_color = open_color_dialog(current_color)
self.bookToolBar.colorBoxBG.setStyleSheet(
'background-color: %s' % new_color.name())
current_profile['background'] = new_color
elif signal_sender == 'comicBGColor':
current_color = self.comic_profile['background']
new_color = open_color_dialog(current_color)
self.bookToolBar.comicBGColor.setStyleSheet(
'background-color: %s' % new_color.name())
self.comic_profile['background'] = new_color
self.bookToolBar.profileBox.setItemData(
profile_index, current_profile, QtCore.Qt.UserRole)
self.format_contentView()
def modify_font(self, signal_sender):
profile_index = self.bookToolBar.profileBox.currentIndex()
current_profile = self.bookToolBar.profileBox.itemData(
profile_index, QtCore.Qt.UserRole)
if signal_sender == 'fontBox':
current_profile['font'] = self.bookToolBar.fontBox.currentFont().family()
if signal_sender == 'fontSizeBox':
old_size = current_profile['font_size']
new_size = self.bookToolBar.fontSizeBox.itemText(
self.bookToolBar.fontSizeBox.currentIndex())
if new_size.isdigit():
current_profile['font_size'] = new_size
else:
current_profile['font_size'] = old_size
if signal_sender == 'lineSpacingUp' and current_profile['line_spacing'] < 200:
current_profile['line_spacing'] += 5
if signal_sender == 'lineSpacingDown' and current_profile['line_spacing'] > 90:
current_profile['line_spacing'] -= 5
if signal_sender == 'paddingUp':
current_profile['padding'] += 5
if signal_sender == 'paddingDown':
current_profile['padding'] -= 5
alignment_dict = {
'alignLeft': 'left',
'alignRight': 'right',
'alignCenter': 'center',
'alignJustify': 'justify'}
if signal_sender in alignment_dict:
current_profile['text_alignment'] = alignment_dict[signal_sender]
self.bookToolBar.profileBox.setItemData(
profile_index, current_profile, QtCore.Qt.UserRole)
self.format_contentView()
def modify_comic_view(self, signal_sender, key_pressed):
comic_profile = self.main_window.comic_profile
current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())
self.bookToolBar.fitWidth.setChecked(False)
self.bookToolBar.bestFit.setChecked(False)
self.bookToolBar.originalSize.setChecked(False)
if signal_sender == 'zoomOut' or key_pressed == QtCore.Qt.Key_Minus:
comic_profile['zoom_mode'] = 'manualZoom'
comic_profile['padding'] += 50
# This prevents infinite zoom out
if comic_profile['padding'] * 2 > current_tab.contentView.viewport().width():
comic_profile['padding'] -= 50
if signal_sender == 'zoomIn' or key_pressed in (
QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
comic_profile['zoom_mode'] = 'manualZoom'
comic_profile['padding'] -= 50
# This prevents infinite zoom in
if comic_profile['padding'] < 0:
comic_profile['padding'] = 0
if signal_sender == 'fitWidth' or key_pressed == QtCore.Qt.Key_W:
comic_profile['zoom_mode'] = 'fitWidth'
comic_profile['padding'] = 0
self.bookToolBar.fitWidth.setChecked(True)
# Padding in the following cases is decided by
# the image pixmap loaded by the widget
if signal_sender == 'bestFit' or key_pressed == QtCore.Qt.Key_B:
comic_profile['zoom_mode'] = 'bestFit'
self.bookToolBar.bestFit.setChecked(True)
if signal_sender == 'originalSize' or key_pressed == QtCore.Qt.Key_O:
comic_profile['zoom_mode'] = 'originalSize'
self.bookToolBar.originalSize.setChecked(True)
self.format_contentView()
def format_contentView(self):
current_tab = self.tabWidget.currentWidget()
try:
current_metadata = current_tab.metadata
except AttributeError:
return
if current_metadata['images_only']:
background = self.comic_profile['background']
zoom_mode = self.comic_profile['zoom_mode']
if zoom_mode == 'fitWidth':
self.bookToolBar.fitWidth.setChecked(True)
if zoom_mode == 'bestFit':
self.bookToolBar.bestFit.setChecked(True)
if zoom_mode == 'originalSize':
self.bookToolBar.originalSize.setChecked(True)
self.bookToolBar.comicBGColor.setStyleSheet(
'background-color: %s' % background.name())
current_tab.format_view(
None, None, None, background, None, None, None)
else:
profile_index = self.bookToolBar.profileBox.currentIndex()
current_profile = self.bookToolBar.profileBox.itemData(
profile_index, QtCore.Qt.UserRole)
font = current_profile['font']
foreground = current_profile['foreground']
background = current_profile['background']
padding = current_profile['padding']
font_size = current_profile['font_size']
line_spacing = current_profile['line_spacing']
text_alignment = current_profile['text_alignment']
# Change toolbar widgets to match new settings
self.bookToolBar.fontBox.blockSignals(True)
self.bookToolBar.fontSizeBox.blockSignals(True)
self.bookToolBar.fontBox.setCurrentText(font)
current_index = self.bookToolBar.fontSizeBox.findText(
str(font_size), QtCore.Qt.MatchExactly)
self.bookToolBar.fontSizeBox.setCurrentIndex(current_index)
self.bookToolBar.fontBox.blockSignals(False)
self.bookToolBar.fontSizeBox.blockSignals(False)
self.alignment_dict[current_profile['text_alignment']].setChecked(True)
self.bookToolBar.colorBoxFG.setStyleSheet(
'background-color: %s' % foreground.name())
self.bookToolBar.colorBoxBG.setStyleSheet(
'background-color: %s' % background.name())
current_tab.format_view(
font, font_size, foreground,
background, padding, line_spacing,
text_alignment)
def reset_profile(self):
current_profile_index = self.bookToolBar.profileBox.currentIndex()
current_profile_default = Settings(self).default_profiles[current_profile_index]
self.bookToolBar.profileBox.setItemData(
current_profile_index, current_profile_default, QtCore.Qt.UserRole)
self.format_contentView()

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader # This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga # Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -18,52 +16,62 @@
import os import os
import pickle import pickle
import logging
import pathlib import pathlib
from PyQt5 import QtGui, QtCore from PyQt5 import QtGui, QtCore
from lector import database from lector import database
from lector.models import TableProxyModel, ItemProxyModel from lector.models import TableProxyModel, ItemProxyModel
logger = logging.getLogger(__name__)
class Library: class Library:
def __init__(self, parent): def __init__(self, parent):
self.parent = parent self.main_window = parent
self.view_model = None self.libraryModel = None
self.item_proxy_model = None self.itemProxyModel = None
self.table_proxy_model = None self.tableProxyModel = None
self._translate = QtCore.QCoreApplication.translate
def generate_model(self, mode, parsed_books=None, is_database_ready=True): def generate_model(self, mode, parsed_books=None, is_database_ready=True):
if mode == 'build': if mode == 'build':
self.view_model = QtGui.QStandardItemModel() self.libraryModel = QtGui.QStandardItemModel()
self.view_model.setColumnCount(10) self.libraryModel.setColumnCount(10)
books = database.DatabaseFunctions( books = database.DatabaseFunctions(
self.parent.database_path).fetch_data( self.main_window.database_path).fetch_data(
('Title', 'Author', 'Year', 'DateAdded', 'Path', ('Title', 'Author', 'Year', 'DateAdded', 'Path',
'Position', 'ISBN', 'Tags', 'Hash', 'LastAccessed'), 'Position', 'ISBN', 'Tags', 'Hash', 'LastAccessed',
'Addition'),
'books', 'books',
{'Title': ''}, {'Title': ''},
'LIKE') 'LIKE')
if not books: if not books:
print('Database returned nothing') logger.warning('Database returned nothing')
return return
elif mode == 'addition': elif mode == 'addition':
# Assumes self.view_model already exists and may be extended # Assumes self.libraryModel already exists and may be extended
# Because any additional books have already been added to the # Because any additional books have already been added to the
# database using background threads # database using background threads
books = [] books = []
current_qdatetime = QtCore.QDateTime().currentDateTime() current_qdatetime = QtCore.QDateTime().currentDateTime()
for i in parsed_books.items(): for i in parsed_books.items():
_tags = i[1]['tags'] try:
if _tags: _tags = i[1]['tags']
_tags = ', '.join([j for j in _tags if j]) if _tags:
_tags = ', '.join([j for j in _tags if j])
except: # Continuing seems more important than being correct
_tags = []
logger.warning('Tag generation error for: ' + i[1]['path'])
books.append([ books.append([
i[1]['title'], i[1]['author'], i[1]['year'], current_qdatetime, i[1]['title'], i[1]['author'], i[1]['year'], current_qdatetime,
i[1]['path'], None, i[1]['isbn'], _tags, i[0], None]) i[1]['path'], None, i[1]['isbn'], _tags, i[0], None, i[1]['addition_mode']])
else: else:
return return
@@ -75,7 +83,11 @@ class Library:
author = i[1] author = i[1]
year = i[2] year = i[2]
path = i[4] path = i[4]
addition_mode = i[10]
last_accessed = i[9] last_accessed = i[9]
if last_accessed and not isinstance(last_accessed, QtCore.QDateTime):
last_accessed = pickle.loads(last_accessed)
tags = i[7] tags = i[7]
if isinstance(tags, list): # When files are added for the first time if isinstance(tags, list): # When files are added for the first time
@@ -93,16 +105,12 @@ class Library:
position = i[5] position = i[5]
if position: if position:
position = pickle.loads(position) position = pickle.loads(position)
if position['is_read']: position_perc = generate_position_percentage(position)
position_perc = 100
else:
try:
position_perc = (
position['current_chapter'] * 100 / position['total_chapters'])
except KeyError:
position_perc = None
file_exists = os.path.exists(path) try:
file_exists = os.path.exists(path)
except UnicodeEncodeError:
print('Library: Unicode encoding error')
all_metadata = { all_metadata = {
'title': title, 'title': title,
@@ -115,9 +123,12 @@ class Library:
'tags': tags, 'tags': tags,
'hash': i[8], 'hash': i[8],
'last_accessed': last_accessed, 'last_accessed': last_accessed,
'addition_mode': addition_mode,
'file_exists': file_exists} 'file_exists': file_exists}
tooltip_string = title + '\nAuthor: ' + author + '\nYear: ' + str(year) author_string = self._translate('Library', 'Author')
year_string = self._translate('Library', 'Year')
tooltip_string = f'{title} \n{author_string}: {author} \n{year_string}: {str(year)}'
# Additional data can be set using an incrementing # Additional data can be set using an incrementing
# QtCore.Qt.UserRole # QtCore.Qt.UserRole
@@ -134,6 +145,7 @@ class Library:
item.setToolTip(tooltip_string) item.setToolTip(tooltip_string)
# Just keep the following order. It's way too much trouble otherwise # Just keep the following order. It's way too much trouble otherwise
# User roles have to be correlated to sorting order below
item.setData(title, QtCore.Qt.UserRole) item.setData(title, QtCore.Qt.UserRole)
item.setData(author, QtCore.Qt.UserRole + 1) item.setData(author, QtCore.Qt.UserRole + 1)
item.setData(year, QtCore.Qt.UserRole + 2) item.setData(year, QtCore.Qt.UserRole + 2)
@@ -145,53 +157,61 @@ class Library:
item.setData(False, QtCore.Qt.UserRole + 8) # Is the cover being displayed? item.setData(False, QtCore.Qt.UserRole + 8) # Is the cover being displayed?
item.setData(date_added, QtCore.Qt.UserRole + 9) item.setData(date_added, QtCore.Qt.UserRole + 9)
item.setData(last_accessed, QtCore.Qt.UserRole + 12) item.setData(last_accessed, QtCore.Qt.UserRole + 12)
item.setData(path, QtCore.Qt.UserRole + 13)
item.setIcon(QtGui.QIcon(img_pixmap)) item.setIcon(QtGui.QIcon(img_pixmap))
self.view_model.appendRow(item) self.libraryModel.appendRow(item)
# The is_database_ready boolean is required when a new thread sends # The is_database_ready boolean is required when a new thread sends
# books here for model generation. # books here for model generation.
if not self.parent.settings['perform_culling'] and is_database_ready: if not self.main_window.settings['perform_culling'] and is_database_ready:
self.parent.load_all_covers() self.main_window.cover_functions.load_all_covers()
def generate_proxymodels(self): def generate_proxymodels(self):
self.item_proxy_model = ItemProxyModel() self.itemProxyModel = ItemProxyModel()
self.item_proxy_model.setSourceModel(self.view_model) self.itemProxyModel.setSourceModel(self.libraryModel)
self.item_proxy_model.setSortCaseSensitivity(False) self.itemProxyModel.setSortCaseSensitivity(False)
s = QtCore.QSize(160, 250) # Set icon sizing here s = QtCore.QSize(160, 250) # Set icon sizing here
self.parent.listView.setIconSize(s) self.main_window.listView.setIconSize(s)
self.parent.listView.setModel(self.item_proxy_model) self.main_window.listView.setModel(self.itemProxyModel)
self.table_proxy_model = TableProxyModel(self.parent.temp_dir.path()) self.tableProxyModel = TableProxyModel(
self.table_proxy_model.setSourceModel(self.view_model) self.main_window.temp_dir.path(),
self.table_proxy_model.setSortCaseSensitivity(False) self.main_window.tableView.horizontalHeader(),
self.parent.tableView.setModel(self.table_proxy_model) self.main_window.settings['consider_read_at'])
self.tableProxyModel.setSourceModel(self.libraryModel)
self.tableProxyModel.setSortCaseSensitivity(False)
self.main_window.tableView.setModel(self.tableProxyModel)
self.update_proxymodels() self.update_proxymodels()
def update_proxymodels(self): def update_proxymodels(self):
# Table proxy model # Table proxy model
self.table_proxy_model.invalidateFilter() self.tableProxyModel.invalidateFilter()
self.table_proxy_model.setFilterParams( self.tableProxyModel.setFilterParams(
self.parent.libraryToolBar.searchBar.text(), self.main_window.libraryToolBar.searchBar.text(),
self.parent.active_library_filters, self.main_window.active_library_filters,
0) # This doesn't need to know the sorting box position 0) # This doesn't need to know the sorting box position
self.table_proxy_model.setFilterFixedString( self.tableProxyModel.setFilterFixedString(
self.parent.libraryToolBar.searchBar.text()) self.main_window.libraryToolBar.searchBar.text())
# ^^^ This isn't needed, but it forces a model update every time the # ^^^ This isn't needed, but it forces a model update every time the
# text in the line edit changes. So I guess it is needed. # text in the line edit changes. So I guess it is needed.
self.tableProxyModel.sort_table_columns(
self.main_window.tableView.horizontalHeader().sortIndicatorSection())
self.tableProxyModel.sort_table_columns()
# Item proxy model # Item proxy model
self.item_proxy_model.invalidateFilter() self.itemProxyModel.invalidateFilter()
self.item_proxy_model.setFilterParams( self.itemProxyModel.setFilterParams(
self.parent.libraryToolBar.searchBar.text(), self.main_window.libraryToolBar.searchBar.text(),
self.parent.active_library_filters, self.main_window.active_library_filters,
self.parent.libraryToolBar.sortingBox.currentIndex()) self.main_window.libraryToolBar.sortingBox.currentIndex())
self.item_proxy_model.setFilterFixedString( self.itemProxyModel.setFilterFixedString(
self.parent.libraryToolBar.searchBar.text()) self.main_window.libraryToolBar.searchBar.text())
self.parent.statusMessage.setText( self.main_window.statusMessage.setText(
str(self.item_proxy_model.rowCount()) + ' books') str(self.itemProxyModel.rowCount()) +
self._translate('Library', ' books'))
# TODO # TODO
# Allow sorting by type # Allow sorting by type
@@ -205,33 +225,46 @@ class Library:
1: 1, 1: 1,
2: 2, 2: 2,
3: 9, 3: 9,
4: 12} 4: 12,
5: 7}
# Sorting according to roles and the drop down in the library toolbar # Sorting according to roles and the drop down in the library toolbar
self.item_proxy_model.setSortRole( self.itemProxyModel.setSortRole(
QtCore.Qt.UserRole + sort_roles[self.parent.libraryToolBar.sortingBox.currentIndex()]) QtCore.Qt.UserRole +
sort_roles[self.main_window.libraryToolBar.sortingBox.currentIndex()])
# This can be expanded to other fields by appending to the list # This can be expanded to other fields by appending to the list
sort_order = QtCore.Qt.AscendingOrder sort_order = QtCore.Qt.AscendingOrder
if self.parent.libraryToolBar.sortingBox.currentIndex() in [3, 4]: if self.main_window.libraryToolBar.sortingBox.currentIndex() in [3, 4, 5]:
sort_order = QtCore.Qt.DescendingOrder sort_order = QtCore.Qt.DescendingOrder
self.item_proxy_model.sort(0, sort_order) self.itemProxyModel.sort(0, sort_order)
self.parent.start_culling_timer() self.main_window.start_culling_timer()
def generate_library_tags(self): def generate_library_tags(self):
db_library_directories = database.DatabaseFunctions( db_library_directories = database.DatabaseFunctions(
self.parent.database_path).fetch_data( self.main_window.database_path).fetch_data(
('Path', 'Name', 'Tags'), ('Path', 'Name', 'Tags'),
'directories', # This checks the directories table NOT the book one 'directories', # This checks the directories table NOT the book one
{'Path': ''}, {'Path': ''},
'LIKE') 'LIKE')
if not db_library_directories: # Empty database / table if db_library_directories: # Empty database / table
return library_directories = {
i[0]: (i[1], i[2]) for i in db_library_directories}
library_directories = { else:
i[0]: (i[1], i[2]) for i in db_library_directories} db_library_directories = database.DatabaseFunctions(
self.main_window.database_path).fetch_data(
('Path',),
'books', # THIS CHECKS THE BOOKS TABLE
{'Path': ''},
'LIKE')
library_directories = None
if db_library_directories:
library_directories = {
i[0]: (None, None) for i in db_library_directories}
def get_tags(all_metadata): def get_tags(all_metadata):
path = os.path.dirname(all_metadata['path']) path = os.path.dirname(all_metadata['path'])
@@ -243,7 +276,7 @@ class Library:
if directory_name: if directory_name:
directory_name = directory_name.lower() directory_name = directory_name.lower()
else: else:
directory_name = path.rsplit('/')[-1].lower() directory_name = i.rsplit(os.sep)[-1].lower()
directory_tags = library_directories[i][1] directory_tags = library_directories[i][1]
if directory_tags: if directory_tags:
@@ -251,11 +284,15 @@ class Library:
return directory_name, directory_tags return directory_name, directory_tags
return 'manually added', None # A file is assigned a 'manually added' tag in case it isn't
# in any designated library directory
added_string = self._translate('Library', 'manually added')
return added_string.lower(), None
# Generate tags for the QStandardItemModel # Generate tags for the QStandardItemModel
for i in range(self.view_model.rowCount()): # This isn't triggered for an empty view model
this_item = self.view_model.item(i, 0) for i in range(self.libraryModel.rowCount()):
this_item = self.libraryModel.item(i, 0)
all_metadata = this_item.data(QtCore.Qt.UserRole + 3) all_metadata = this_item.data(QtCore.Qt.UserRole + 3)
directory_name, directory_tags = get_tags(all_metadata) directory_name, directory_tags = get_tags(all_metadata)
@@ -267,30 +304,51 @@ class Library:
# All files in unselected directories will have to be removed # All files in unselected directories will have to be removed
# from both of the models # from both of the models
# They will also have to be deleted from the library # They will also have to be deleted from the library
valid_paths = set(valid_paths) invalid_paths = []
deletable_persistent_indexes = []
for i in range(self.libraryModel.rowCount()):
item = self.libraryModel.item(i)
# Get all paths
all_paths = set()
for i in range(self.view_model.rowCount()):
item = self.view_model.item(i, 0)
item_metadata = item.data(QtCore.Qt.UserRole + 3) item_metadata = item.data(QtCore.Qt.UserRole + 3)
book_path = item_metadata['path'] book_path = item_metadata['path']
all_paths.add(book_path) try:
addition_mode = item_metadata['addition_mode']
except KeyError:
addition_mode = 'automatic'
logger.error('Libary: Error setting addition mode for prune')
invalid_paths = all_paths - valid_paths if (book_path not in valid_paths and
(addition_mode != 'manual' or addition_mode is None)):
deletable_persistent_indexes = [] invalid_paths.append(book_path)
for i in range(self.view_model.rowCount()):
item = self.view_model.item(i)
path = item.data(QtCore.Qt.UserRole + 3)['path']
if path in invalid_paths:
deletable_persistent_indexes.append( deletable_persistent_indexes.append(
QtCore.QPersistentModelIndex(item.index())) QtCore.QPersistentModelIndex(item.index()))
if deletable_persistent_indexes: if deletable_persistent_indexes:
for i in deletable_persistent_indexes: for i in deletable_persistent_indexes:
self.view_model.removeRow(i.row()) self.libraryModel.removeRow(i.row())
# Remove invalid paths from the database as well # Remove invalid paths from the database as well
database.DatabaseFunctions( database.DatabaseFunctions(
self.parent.database_path).delete_from_database('Path', invalid_paths) self.main_window.database_path).delete_from_database('Path', invalid_paths)
def generate_position_percentage(position):
if not position:
return None
if position['is_read']:
position_perc = 1
else:
try:
position_perc = (
position['current_block'] / position['total_blocks'])
except (KeyError, ZeroDivisionError):
try:
position_perc = (
position['current_chapter'] / position['total_chapters'])
except KeyError:
position_perc = None
return position_perc

58
lector/logger.py Normal file
View File

@@ -0,0 +1,58 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
VERSION = '0.5.GittyGittyBangBang'
import os
import logging
from PyQt5 import QtCore
location_prefix = os.path.join(
QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation),
'Lector')
logger_filename = os.path.join(location_prefix, 'Lector.log')
def init_logging(cli_arguments):
# This needs a separate 'Lector' in the os.path.join because
# application name isn't explicitly set in this module
os.makedirs(location_prefix, exist_ok=True)
log_level = 30 # Warning and above
# Set log level according to command line arguments
try:
if cli_arguments[1] == 'debug':
log_level = 10 # Debug and above
print('Debug logging enabled')
try:
os.remove(logger_filename) # Remove old log for clarity
except FileNotFoundError:
pass
except IndexError:
pass
# Create logging object
logging.basicConfig(
filename=logger_filename,
filemode='a',
format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s',
datefmt='%Y/%m/%d %H:%M:%S',
level=log_level)
logging.addLevelName(60, 'HAMMERTIME') ## Messages that MUST be logged
return logging.getLogger('lector.main')

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader # This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga # Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -16,18 +14,22 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5 import QtWidgets, QtCore, QtGui
from lector import database from lector import database
from resources import metadata
from lector.widgets import PliantQGraphicsScene from lector.widgets import PliantQGraphicsScene
from lector.resources import metadata
logger = logging.getLogger(__name__)
class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog): class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
def __init__(self, parent): def __init__(self, parent):
super(MetadataUI, self).__init__() super(MetadataUI, self).__init__()
self.setupUi(self) self.setupUi(self)
self._translate = QtCore.QCoreApplication.translate
self.setWindowFlags( self.setWindowFlags(
QtCore.Qt.Popup | QtCore.Qt.Popup |
@@ -38,8 +40,12 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
radius = 15 radius = 15
path = QtGui.QPainterPath() path = QtGui.QPainterPath()
path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius) path.addRoundedRect(QtCore.QRectF(self.rect()), radius, radius)
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask) try:
mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
self.setMask(mask)
except TypeError: # Required for older versions of Qt
pass
self.parent = parent self.parent = parent
self.database_path = self.parent.database_path self.database_path = self.parent.database_path
@@ -85,8 +91,8 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
graphics_scene.addPixmap(image_pixmap) graphics_scene.addPixmap(image_pixmap)
self.coverView.setScene(graphics_scene) self.coverView.setScene(graphics_scene)
def ok_pressed(self, event): def ok_pressed(self, event=None):
book_item = self.parent.lib_ref.view_model.item(self.book_index.row()) book_item = self.parent.lib_ref.libraryModel.item(self.book_index.row())
title = self.titleLine.text() title = self.titleLine.text()
author = self.authorLine.text() author = self.authorLine.text()
@@ -97,7 +103,9 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
except ValueError: except ValueError:
year = self.book_year year = self.book_year
tooltip_string = title + '\nAuthor: ' + author + '\nYear: ' + str(year) author_string = self._translate('MetadataUI', 'Author')
year_string = self._translate('MetadataUI', 'Year')
tooltip_string = f'{title} \n{author_string}: {author} \n{year_string}: {str(year)}'
book_item.setData(title, QtCore.Qt.UserRole) book_item.setData(title, QtCore.Qt.UserRole)
book_item.setData(author, QtCore.Qt.UserRole + 1) book_item.setData(author, QtCore.Qt.UserRole + 1)
@@ -114,7 +122,7 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
if self.cover_for_database: if self.cover_for_database:
database_dict['CoverImage'] = self.cover_for_database database_dict['CoverImage'] = self.cover_for_database
self.parent.cover_loader( self.parent.cover_functions.cover_loader(
book_item, self.cover_for_database) book_item, self.cover_for_database)
self.parent.lib_ref.update_proxymodels() self.parent.lib_ref.update_proxymodels()
@@ -123,7 +131,7 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
database.DatabaseFunctions(self.database_path).modify_metadata( database.DatabaseFunctions(self.database_path).modify_metadata(
database_dict, book_hash) database_dict, book_hash)
def cancel_pressed(self, event): def cancel_pressed(self, event=None):
self.hide() self.hide()
def generate_display_position(self, mouse_cursor_position): def generate_display_position(self, mouse_cursor_position):
@@ -146,7 +154,8 @@ class MetadataUI(QtWidgets.QDialog, metadata.Ui_Dialog):
background = self.parent.settings['dialog_background'] background = self.parent.settings['dialog_background']
else: else:
self.previous_position = self.pos() self.previous_position = self.pos()
background = self.parent.get_color() self.parent.get_color()
background = self.parent.settings['dialog_background']
self.setStyleSheet( self.setStyleSheet(
"QDialog {{background-color: {0}}}".format(background.name())) "QDialog {{background-color: {0}}}".format(background.name()))

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader # This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga # Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -16,34 +14,32 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import pickle import logging
import pathlib import pathlib
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from resources import pie_chart from lector.resources import pie_chart
logger = logging.getLogger(__name__)
class BookmarkProxyModel(QtCore.QSortFilterProxyModel): class BookmarkProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None): def __init__(self, parent=None):
super(BookmarkProxyModel, self).__init__(parent) super(BookmarkProxyModel, self).__init__(parent)
self.parent = parent self.parent = parent
self.filter_string = None self.parentTab = self.parent.parent
self.filter_text = None
def setFilterParams(self, filter_text): def setFilterParams(self, filter_text):
self.filter_text = filter_text self.filter_text = filter_text
def filterAcceptsRow(self, row, parent):
# TODO
# Connect this to the search bar
return True
def setData(self, index, value, role): def setData(self, index, value, role):
if role == QtCore.Qt.EditRole: if role == QtCore.Qt.EditRole:
source_index = self.mapToSource(index) source_index = self.mapToSource(index)
identifier = self.sourceModel().data(source_index, QtCore.Qt.UserRole + 2) identifier = self.sourceModel().data(source_index, QtCore.Qt.UserRole + 2)
self.sourceModel().setData(source_index, value, QtCore.Qt.DisplayRole) self.sourceModel().setData(source_index, value, QtCore.Qt.DisplayRole)
self.parent.metadata['bookmarks'][identifier]['description'] = value self.parentTab.metadata['bookmarks'][identifier]['description'] = value
return True return True
@@ -66,10 +62,21 @@ class ItemProxyModel(QtCore.QSortFilterProxyModel):
class TableProxyModel(QtCore.QSortFilterProxyModel): class TableProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, temp_dir, parent=None): def __init__(self, temp_dir, tableViewHeader, consider_read_at, parent=None):
super(TableProxyModel, self).__init__(parent) super(TableProxyModel, self).__init__(parent)
self.tableViewHeader = tableViewHeader
self.consider_read_at = consider_read_at
self._translate = QtCore.QCoreApplication.translate
title_string = self._translate('TableProxyModel', 'Title')
author_string = self._translate('TableProxyModel', 'Author')
year_string = self._translate('TableProxyModel', 'Year')
lastread_string = self._translate('TableProxyModel', 'Last Read')
tags_string = self._translate('TableProxyModel', 'Tags')
self.header_data = [ self.header_data = [
None, 'Title', 'Author', 'Year', 'Last Read', '%', 'Tags'] None, title_string, author_string,
year_string, lastread_string, '%', tags_string]
self.temp_dir = temp_dir self.temp_dir = temp_dir
self.filter_text = None self.filter_text = None
self.active_library_filters = None self.active_library_filters = None
@@ -88,7 +95,13 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
def headerData(self, column, orientation, role): def headerData(self, column, orientation, role):
if role == QtCore.Qt.DisplayRole: if role == QtCore.Qt.DisplayRole:
return self.header_data[column] try:
return self.header_data[column]
except IndexError:
logger.error(
'Table proxy model: Can\'t find header for column' + str(column))
# The column will be called IndexError. Not a typo.
return 'IndexError'
def flags(self, index): def flags(self, index):
# Tag editing will take place by way of a right click menu # Tag editing will take place by way of a right click menu
@@ -108,46 +121,27 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
return_pixmap = None return_pixmap = None
file_exists = item.data(QtCore.Qt.UserRole + 5) file_exists = item.data(QtCore.Qt.UserRole + 5)
metadata = item.data(QtCore.Qt.UserRole + 3) position_percent = item.data(QtCore.Qt.UserRole + 7)
position = metadata['position']
if position:
is_read = position['is_read']
if not file_exists: if not file_exists:
return pie_chart.pixmapper( return pie_chart.pixmapper(
-1, None, None, QtCore.Qt.SizeHintRole + 10) -1, None, None, QtCore.Qt.SizeHintRole + 10)
if position: if position_percent:
if is_read:
current_chapter = total_chapters = 100
else:
try:
current_chapter = position['current_chapter']
total_chapters = position['total_chapters']
# TODO
# See if there's any rationale for this
if current_chapter == 1:
raise KeyError
except KeyError:
return
return_pixmap = pie_chart.pixmapper( return_pixmap = pie_chart.pixmapper(
current_chapter, total_chapters, self.temp_dir, position_percent, self.temp_dir,
self.consider_read_at,
QtCore.Qt.SizeHintRole + 10) QtCore.Qt.SizeHintRole + 10)
return return_pixmap return return_pixmap
elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
if index.column() in (0, 5): # Cover and Status if index.column() in (0, 5): # Cover and Status
return QtCore.QVariant() return QtCore.QVariant()
if index.column() == 4: if index.column() == 4:
last_accessed_time = item.data(self.role_dictionary[index.column()]) last_accessed = item.data(self.role_dictionary[index.column()])
if last_accessed_time: if last_accessed:
last_accessed = last_accessed_time
if not isinstance(last_accessed_time, QtCore.QDateTime):
last_accessed = pickle.loads(last_accessed_time)
right_now = QtCore.QDateTime().currentDateTime() right_now = QtCore.QDateTime().currentDateTime()
time_diff = last_accessed.msecsTo(right_now) time_diff = last_accessed.msecsTo(right_now)
return self.time_convert(time_diff // 1000) return self.time_convert(time_diff // 1000)
@@ -164,10 +158,13 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
output = self.common_functions.filterAcceptsRow(row, parent) output = self.common_functions.filterAcceptsRow(row, parent)
return output return output
def sort_table_columns(self, column): def sort_table_columns(self, column=None):
sorting_order = self.sender().sortIndicatorOrder() column = self.tableViewHeader.sortIndicatorSection()
sorting_order = self.tableViewHeader.sortIndicatorOrder()
self.sort(0, sorting_order) self.sort(0, sorting_order)
self.setSortRole(self.role_dictionary[column]) if column != 0:
self.setSortRole(self.role_dictionary[column])
def time_convert(self, seconds): def time_convert(self, seconds):
seconds = int(seconds) seconds = int(seconds)
@@ -201,14 +198,20 @@ class ProxyModelsCommonFunctions:
title = model.data(this_index, QtCore.Qt.UserRole) title = model.data(this_index, QtCore.Qt.UserRole)
author = model.data(this_index, QtCore.Qt.UserRole + 1) author = model.data(this_index, QtCore.Qt.UserRole + 1)
tags = model.data(this_index, QtCore.Qt.UserRole + 4) tags = model.data(this_index, QtCore.Qt.UserRole + 4)
progress = model.data(this_index, QtCore.Qt.UserRole + 7)
directory_name = model.data(this_index, QtCore.Qt.UserRole + 10) directory_name = model.data(this_index, QtCore.Qt.UserRole + 10)
directory_tags = model.data(this_index, QtCore.Qt.UserRole + 11) directory_tags = model.data(this_index, QtCore.Qt.UserRole + 11)
last_accessed = model.data(this_index, QtCore.Qt.UserRole + 12) last_accessed = model.data(this_index, QtCore.Qt.UserRole + 12)
file_path = model.data(this_index, QtCore.Qt.UserRole + 13)
# Hide untouched files when sorting by last accessed # Hide untouched files when sorting by last accessed
if self.parent_model.sorting_box_position == 4 and not last_accessed: if self.parent_model.sorting_box_position == 4 and not last_accessed:
return False return False
# Hide untouched files when sorting by progress
if self.parent_model.sorting_box_position == 5 and not progress:
return False
if self.parent_model.active_library_filters: if self.parent_model.active_library_filters:
if directory_name not in self.parent_model.active_library_filters: if directory_name not in self.parent_model.active_library_filters:
return False return False
@@ -220,7 +223,9 @@ class ProxyModelsCommonFunctions:
else: else:
valid_data = [ valid_data = [
i.lower() for i in ( i.lower() for i in (
title, author, tags, directory_name, directory_tags) if i is not None] title, author, tags, directory_name,
directory_tags, file_path)
if i is not None]
for i in valid_data: for i in valid_data:
if self.parent_model.filter_text.lower() in i: if self.parent_model.filter_text.lower() in i:
return True return True
@@ -338,32 +343,3 @@ class MostExcellentFileSystemModel(QtWidgets.QFileSystemModel):
for i in deletable: for i in deletable:
del self.tag_data[i] del self.tag_data[i]
# TODO
# Unbork this
class FileSystemProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super(FileSystemProxyModel, self).__init__(parent)
def filterAcceptsRow(self, row_num, parent):
model = self.sourceModel()
filter_out = [
'boot', 'dev', 'etc', 'lost+found', 'opt', 'pdb',
'proc', 'root', 'run', 'srv', 'sys', 'tmp', 'twonky',
'usr', 'var', 'bin', 'kdeinit5__0', 'lib', 'lib64', 'sbin']
name_index = model.index(row_num, 0)
valid_data = model.data(name_index)
print(valid_data)
return True
try:
if valid_data in filter_out:
return False
except AttributeError:
pass
return True

View File

@@ -0,0 +1,80 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-19 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# TODO
# Account for files with passwords
import os
import time
import logging
import zipfile
import collections
from lector.rarfile import rarfile
logger = logging.getLogger(__name__)
class ParseCOMIC:
def __init__(self, filename, *args):
self.filename = filename
self.book = None
self.image_list = None
self.book_extension = os.path.splitext(self.filename)
def read_book(self):
if self.book_extension[1] == '.cbz':
self.book = zipfile.ZipFile(
self.filename, mode='r', allowZip64=True)
self.image_list = [
i.filename for i in self.book.infolist()
if not i.is_dir() and is_image(i.filename)]
elif self.book_extension[1] == '.cbr':
self.book = rarfile.RarFile(self.filename)
self.image_list = [
i.filename for i in self.book.infolist()
if not i.isdir() and is_image(i.filename)]
self.image_list.sort()
def generate_metadata(self):
title = os.path.basename(self.book_extension[0]).strip(' ')
author = '<Unknown>'
isbn = None
tags = []
cover = self.book.read(self.image_list[0])
creation_time = time.ctime(os.path.getctime(self.filename))
year = creation_time.split()[-1]
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
return Metadata(title, author, year, isbn, tags, cover)
def generate_content(self):
image_number = len(self.image_list)
toc = [(1, f'Page {i + 1}', i + 1) for i in range(image_number)]
# Return toc, content, images_only
return toc, self.image_list, True
def is_image(filename):
valid_image_extensions = ['.png', '.jpg', '.bmp']
if os.path.splitext(filename)[1].lower() in valid_image_extensions:
return True
else:
return False

98
lector/parsers/djvu.py Normal file
View File

@@ -0,0 +1,98 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import collections
import djvu.decode
from PyQt5 import QtGui
class ParseDJVU:
def __init__(self, filename, *args):
self.book = None
self.filename = filename
def read_book(self):
self.book = djvu.decode.Context().new_document(
djvu.decode.FileURI(self.filename))
self.book.decoding_job.wait()
def generate_metadata(self):
# TODO
# What even is this?
title = os.path.basename(self.filename)
author = 'Unknown'
year = 9999
isbn = None
tags = []
cover_page = self.book.pages[0]
cover = render_djvu_page(cover_page, True)
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
return Metadata(title, author, year, isbn, tags, cover)
def generate_content(self):
# TODO
# See if it's possible to generate a more involved ToC
content = list(range(len(self.book.pages)))
toc = [(1, f'Page {i + 1}', i + 1) for i in content]
# Return toc, content, images_only
return toc, content, True
def render_djvu_page(page, for_cover=False):
# TODO
# Figure out how to calculate image stride
# and if it impacts row_alignment in the render
# method below
# bytes_per_line = 13200
djvu_pixel_format = djvu.decode.PixelFormatRgbMask(
0xFF0000, 0xFF00, 0xFF, bpp=32)
djvu_pixel_format.rows_top_to_bottom = 1
djvu_pixel_format.y_top_to_bottom = 0
# ¯\_(ツ)_/¯
mode = 0
page_job = page.decode(wait=True)
width, height = page_job.size
rect = (0, 0, width, height)
output = page_job.render(
mode, rect, rect, djvu_pixel_format)
# row_alignment=bytes_per_line)
imageFormat = QtGui.QImage.Format_RGB32
pageQImage = QtGui.QImage(output, width, height, imageFormat)
# Format conversion not only keeps the damn thing from
# segfaulting when converting from QImage to QPixmap,
# but it also allows for the double page mode to keep
# working properly. We like format conversion.
pageQImage = pageQImage.convertToFormat(
QtGui.QImage.Format_ARGB32_Premultiplied)
if for_cover:
return pageQImage
pixmap = QtGui.QPixmap()
pixmap.convertFromImage(pageQImage)
return pixmap

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader # This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga # Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -16,51 +14,43 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# TODO
# Maybe also include book description
import os import os
import zipfile import zipfile
import logging
from ePub.read_epub import EPUB from lector.readers.read_epub import EPUB
logger = logging.getLogger(__name__)
class ParseEPUB: class ParseEPUB:
def __init__(self, filename, temp_dir, file_md5): def __init__(self, filename, temp_dir, file_md5):
# TODO
# Maybe also include book description
self.book_ref = None
self.book = None self.book = None
self.filename = filename self.filename = filename
self.temp_dir = temp_dir
self.extract_path = os.path.join(temp_dir, file_md5) self.extract_path = os.path.join(temp_dir, file_md5)
def read_book(self): def read_book(self):
self.book_ref = EPUB(self.filename) self.book = EPUB(self.filename, self.temp_dir)
contents_found = self.book_ref.read_epub()
if not contents_found:
print('Cannot process: ' + self.filename)
return
self.book = self.book_ref.book
def get_title(self): def generate_metadata(self):
return self.book['title'] self.book.generate_metadata()
return self.book.metadata
def get_author(self): def generate_content(self):
return self.book['author']
def get_year(self):
return self.book['year']
def get_cover_image(self):
return self.book['cover']
def get_isbn(self):
return self.book['isbn']
def get_tags(self):
return self.book['tags']
def get_contents(self):
zipfile.ZipFile(self.filename).extractall(self.extract_path) zipfile.ZipFile(self.filename).extractall(self.extract_path)
self.book_ref.parse_chapters(temp_dir=self.extract_path) self.book.generate_toc()
file_settings = { self.book.generate_content()
'images_only': False}
return self.book['book_list'], file_settings toc = []
content = []
for count, i in enumerate(self.book.content):
toc.append((i[0], i[1], count + 1))
content.append(i[2])
# Return toc, content, images_only
return toc, content, False

52
lector/parsers/fb2.py Normal file
View File

@@ -0,0 +1,52 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# TODO
# Maybe also include book description
import os
import logging
from lector.readers.read_fb2 import FB2
logger = logging.getLogger(__name__)
class ParseFB2:
def __init__(self, filename, temp_dir, file_md5):
self.book = None
self.filename = filename
self.extract_path = os.path.join(temp_dir, file_md5)
def read_book(self):
self.book = FB2(self.filename)
def generate_metadata(self):
self.book.generate_metadata()
return self.book.metadata
def generate_content(self):
os.makedirs(self.extract_path, exist_ok=True) # Manual creation is required here
self.book.generate_content(temp_dir=self.extract_path)
toc = []
content = []
for count, i in enumerate(self.book.content):
toc.append((i[0], i[1], count + 1))
content.append(i[2])
# Return toc, content, images_only
return toc, content, False

View File

@@ -0,0 +1,54 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
import collections
import markdown
logger = logging.getLogger(__name__)
class ParseMD:
def __init__(self, filename, *args):
self.book = None
self.filename = filename
def read_book(self):
self.book = None
def generate_metadata(self):
title = os.path.basename(self.filename)
author = 'Unknown'
year = 9999
isbn = None
tags = []
cover = None
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
return Metadata(title, author, year, isbn, tags, cover)
def generate_content(self):
with open(self.filename, 'r') as book:
text = book.read()
content = [markdown.markdown(text)]
toc = [(1, 'Markdown', 1)]
# Return toc, content, images_only
return toc, content, False

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader # This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga # Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -16,78 +14,69 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# This module parses Amazon ebooks using KindleUnpack to first create an # TODO
# epub that is then read the usual way # See if it's possible to just feed the
# unzipped mobi7 file into the EPUB parser module
import os import os
import sys import sys
import shutil import shutil
import zipfile import zipfile
import logging
from ePub.read_epub import EPUB from lector.readers.read_epub import EPUB
import KindleUnpack.kindleunpack as KindleUnpack import lector.KindleUnpack.kindleunpack as KindleUnpack
logger = logging.getLogger(__name__)
class ParseMOBI: class ParseMOBI:
# This module parses Amazon ebooks using KindleUnpack to first create an
# epub and then read the usual way
def __init__(self, filename, temp_dir, file_md5): def __init__(self, filename, temp_dir, file_md5):
self.book_ref = None
self.book = None self.book = None
self.filename = filename self.filename = filename
self.epub_filepath = None self.epub_filepath = None
self.split_large_xml = False
self.temp_dir = temp_dir self.temp_dir = temp_dir
self.extract_dir = os.path.join(temp_dir, file_md5) self.extract_path = os.path.join(temp_dir, file_md5)
def read_book(self): def read_book(self):
with HidePrinting(): with HidePrinting():
KindleUnpack.unpackBook(self.filename, self.extract_dir) KindleUnpack.unpackBook(self.filename, self.extract_path)
epub_filename = os.path.splitext( epub_filename = os.path.splitext(
os.path.basename(self.filename))[0] + '.epub' os.path.basename(self.filename))[0] + '.epub'
self.epub_filepath = os.path.join( self.epub_filepath = os.path.join(
self.extract_dir, 'mobi8', epub_filename) self.extract_path, 'mobi8', epub_filename)
if not os.path.exists(self.epub_filepath): if not os.path.exists(self.epub_filepath):
zip_dir = os.path.join(self.extract_dir, 'mobi7') zip_dir = os.path.join(self.extract_path, 'mobi7')
zip_file = os.path.join( zip_file = os.path.join(
self.extract_dir, epub_filename) self.extract_path, epub_filename)
self.epub_filepath = shutil.make_archive(zip_file, 'zip', zip_dir) self.epub_filepath = shutil.make_archive(zip_file, 'zip', zip_dir)
self.split_large_xml = True
self.book_ref = EPUB(self.epub_filepath) self.book = EPUB(self.epub_filepath, self.temp_dir)
contents_found = self.book_ref.read_epub()
if not contents_found:
print('Cannot process: ' + self.filename)
return
self.book = self.book_ref.book
def get_title(self): def generate_metadata(self):
return self.book['title'] self.book.generate_metadata()
return self.book.metadata
def get_author(self): def generate_content(self):
return self.book['author'] zipfile.ZipFile(self.epub_filepath).extractall(self.extract_path)
def get_year(self): self.book.generate_toc()
return self.book['year'] self.book.generate_content()
def get_cover_image(self): toc = []
return self.book['cover'] content = []
for count, i in enumerate(self.book.content):
toc.append((1, i[1], count + 1))
content.append(i[2])
def get_isbn(self): # Return toc, content, images_only
return self.book['isbn'] return toc, content, False
def get_tags(self):
return self.book['tags']
def get_contents(self):
extract_path = os.path.join(self.extract_dir)
zipfile.ZipFile(self.epub_filepath).extractall(extract_path)
self.book_ref.parse_chapters(
temp_dir=self.temp_dir, split_large_xml=self.split_large_xml)
file_settings = {
'images_only': False}
return self.book['book_list'], file_settings
class HidePrinting: class HidePrinting:
def __enter__(self): def __enter__(self):

100
lector/parsers/pdf.py Normal file
View File

@@ -0,0 +1,100 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import collections
import fitz
from PyQt5 import QtGui
class ParsePDF:
def __init__(self, filename, *args):
self.filename = filename
self.book = None
def read_book(self):
self.book = fitz.open(self.filename)
def generate_metadata(self):
title = self.book.metadata['title']
if not title:
title = os.path.splitext(os.path.basename(self.filename))[0]
author = self.book.metadata['author']
if not author:
author = 'Unknown'
creation_date = self.book.metadata['creationDate']
try:
year = creation_date.split(':')[1][:4]
except (ValueError, AttributeError):
year = 9999
isbn = None
tags = self.book.metadata['keywords']
if not tags:
tags = []
# This is a little roundabout for the cover
# and I'm sure it's taking a performance hit
# But it is simple. So there's that.
cover_page = self.book.loadPage(0)
# Disabling scaling gets the covers much faster
cover = render_pdf_page(cover_page, True)
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
return Metadata(title, author, year, isbn, tags, cover)
def generate_content(self):
content = list(range(self.book.pageCount))
toc = self.book.getToC()
if not toc:
toc = [(1, f'Page {i + 1}', i + 1) for i in range(self.book.pageCount)]
# Return toc, content, images_only
return toc, content, True
def render_pdf_page(page_data, for_cover=False):
# Draw page contents on to a pixmap
# and then return that pixmap
# Render quality is set by the following
zoom_matrix = fitz.Matrix(4, 4)
if for_cover:
zoom_matrix = fitz.Matrix(1, 1)
pagePixmap = page_data.getPixmap(
matrix=zoom_matrix,
alpha=False) # Sets background to White
imageFormat = QtGui.QImage.Format_RGB888 # Set to Format_RGB888 if alpha
pageQImage = QtGui.QImage(
pagePixmap.samples,
pagePixmap.width,
pagePixmap.height,
pagePixmap.stride,
imageFormat)
# The cover page doesn't require conversion into a Pixmap
if for_cover:
return pageQImage
pixmap = QtGui.QPixmap()
pixmap.convertFromImage(pageQImage)
return pixmap

55
lector/parsers/txt.py Normal file
View File

@@ -0,0 +1,55 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import collections
import os
import textile
class ParseTXT:
"""Parser for TXT files."""
def __init__(self, filename, *args):
"""Initialize new instance of the TXT parser."""
self.filename = filename
def read_book(self):
"""Prepare the parser to read book."""
pass
def generate_metadata(self):
"""Generate metadata for the book."""
title = os.path.basename(self.filename)
author = 'Unknown'
year = 9999
isbn = None
tags = []
cover = None
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
return Metadata(title, author, year, isbn, tags, cover)
def generate_content(self):
"""Generate content of the book."""
with open(self.filename, 'rt') as txt:
text = txt.read()
content = [textile.textile(text)]
toc = [(1, 'Text', 1)]
return toc, content, False

478
lector/readers/read_epub.py Normal file
View File

@@ -0,0 +1,478 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# TODO
# See if inserting chapters not in the toc.ncx can be avoided
# Account for stylesheets... eventually
import os
import zipfile
import logging
import collections
from urllib.parse import unquote
import xmltodict
from PyQt5 import QtGui
from bs4 import BeautifulSoup
logger = logging.getLogger(__name__)
class EPUB:
def __init__(self, book_filename, temp_dir):
self.book_filename = book_filename
self.temp_dir = temp_dir
self.zip_file = None
self.file_list = None
self.opf_dict = None
self.cover_image_name = None
self.split_chapters = {}
self.metadata = None
self.content = []
self.generate_references()
def generate_references(self):
self.zip_file = zipfile.ZipFile(
self.book_filename, mode='r', allowZip64=True)
self.file_list = self.zip_file.namelist()
# Book structure relies on parsing the .opf file
# in the book. Now that might be the usual content.opf
# or package.opf or it might be named after your favorite
# eldritch abomination. The point is we have to check
# the container.xml
container = self.find_file('container.xml')
if container:
container_xml = self.zip_file.read(container)
container_dict = xmltodict.parse(container_xml)
packagefile = container_dict['container']['rootfiles']['rootfile']['@full-path']
else:
presumptive_names = ('content.opf', 'package.opf', 'volume.opf')
for i in presumptive_names:
packagefile = self.find_file(i)
if packagefile:
logger.info('Using presumptive package file: ' + self.book_filename)
break
packagefile_data = self.zip_file.read(packagefile)
self.opf_dict = xmltodict.parse(packagefile_data)
def find_file(self, filename):
# Get rid of special characters
filename = unquote(filename)
# First, look for the file in the root of the book
if filename in self.file_list:
return filename
# Then search for it elsewhere
else:
file_basename = os.path.basename(filename)
for i in self.file_list:
if os.path.basename(i) == file_basename:
return i
# If the file isn't found
logger.warning(filename + ' not found in ' + self.book_filename)
return False
def generate_toc(self):
def find_alternative_toc():
toc_filename = None
toc_filename_alternative = None
manifest = self.opf_dict['package']['manifest']['item']
for i in manifest:
# Behold the burning hoops we're jumping through
if i['@id'] == 'ncx':
toc_filename = i['@href']
if ('ncx' in i['@id']) or ('toc' in i['@id']):
toc_filename_alternative = i['@href']
if toc_filename and toc_filename_alternative:
break
if not toc_filename:
if not toc_filename_alternative:
logger.warning('No ToC found for: ' + self.book_filename)
else:
toc_filename = toc_filename_alternative
logger.info('Using alternate ToC for: ' + self.book_filename)
return toc_filename
# Find the toc.ncx file from the manifest
# EPUBs will name literally anything, anything so try
# a less stringent approach if the first one doesn't work
# The idea is to prioritize 'toc.ncx' since this should work
# for the vast majority of books
toc_filename = 'toc.ncx'
does_toc_exist = self.find_file(toc_filename)
if not does_toc_exist:
toc_filename = find_alternative_toc()
tocfile = self.find_file(toc_filename)
tocfile_data = self.zip_file.read(tocfile)
toc_dict = xmltodict.parse(tocfile_data)
def recursor(level, nav_node):
if isinstance(nav_node, list):
these_contents = [[
level + 1,
i['navLabel']['text'],
i['content']['@src']] for i in nav_node]
self.content.extend(these_contents)
return
if 'navPoint' in nav_node.keys():
recursor(level, nav_node['navPoint'])
else:
self.content.append([
level + 1,
nav_node['navLabel']['text'],
nav_node['content']['@src']])
navpoints = toc_dict['ncx']['navMap']['navPoint']
for top_level_nav in navpoints:
# Just one chapter
if isinstance(top_level_nav, str):
self.content.append([
1,
navpoints['navLabel']['text'],
navpoints['content']['@src']])
break
# Multiple chapters
self.content.append([
1,
top_level_nav['navLabel']['text'],
top_level_nav['content']['@src']])
if 'navPoint' in top_level_nav.keys():
recursor(1, top_level_nav)
def get_chapter_content(self, chapter_file):
this_file = self.find_file(chapter_file)
if this_file:
chapter_content = self.zip_file.read(this_file).decode()
# Generate a None return for a blank chapter
# These will be removed from the contents later
contentDocument = QtGui.QTextDocument(None)
contentDocument.setHtml(chapter_content)
contentText = contentDocument.toPlainText().replace('\n', '')
if contentText == '':
chapter_content = None
return chapter_content
else:
return 'Possible parse error: ' + chapter_file
def parse_split_chapters(self, chapters_with_split_content):
# For split chapters, get the whole chapter first, then split
# between ids using their anchors, then "heal" the resultant text
# by creating a BeautifulSoup object. Write its str to the content
for i in chapters_with_split_content.items():
chapter_file = i[0]
self.split_chapters[chapter_file] = {}
chapter_content = self.get_chapter_content(chapter_file)
soup = BeautifulSoup(chapter_content, 'lxml')
split_anchors = i[1]
for this_anchor in reversed(split_anchors):
this_tag = soup.find(
attrs={"id":lambda x: x == this_anchor})
markup_split = str(soup).split(str(this_tag))
soup = BeautifulSoup(markup_split[0], 'lxml')
# If the tag is None, it probably means the content is overlapping
# Skipping the insert is the way forward
if this_tag:
this_markup = BeautifulSoup(
str(this_tag).strip() + markup_split[1], 'lxml')
self.split_chapters[chapter_file][this_anchor] = str(this_markup)
# Remaining markup is assigned here
self.split_chapters[chapter_file]['top_level'] = str(soup)
def generate_content(self):
# Find all the chapters mentioned in the opf spine
# These are simply ids that correspond to the actual item
# as mentioned in the manifest - which is a comprehensive
# list of files
try:
# Multiple chapters
chapters_in_spine = [
i['@idref']
for i in self.opf_dict['package']['spine']['itemref']]
except TypeError:
# Single chapter - Large xml
chapters_in_spine = [
self.opf_dict['package']['spine']['itemref']['@idref']]
# Next, find items and ids from the manifest
# This might error out in case there's only one item in
# the manifest. Remember that for later.
chapters_from_manifest = {
i['@id']: i['@href']
for i in self.opf_dict['package']['manifest']['item']}
# Finally, check which items are supposed to be in the spine
# on the basis of the id and change the toc accordingly
spine_final = []
for i in chapters_in_spine:
try:
spine_final.append(chapters_from_manifest.pop(i))
except KeyError:
pass
toc_chapters = [
unquote(i[2].split('#')[0]) for i in self.content]
for i in spine_final:
if not i in toc_chapters:
spine_index = spine_final.index(i)
if spine_index == 0: # Or chapter insertion circles back to the end
previous_chapter_toc_index = -1
else:
previous_chapter = spine_final[spine_final.index(i) - 1]
previous_chapter_toc_index = toc_chapters.index(previous_chapter)
toc_chapters.insert(
previous_chapter_toc_index + 1, i)
self.content.insert(
previous_chapter_toc_index + 1, [1, None, i])
# Parse split chapters as below
# They can be picked up during the iteration through the toc
chapters_with_split_content = {}
for i in self.content:
if '#' in i[2]:
this_split = i[2].split('#')
chapter = this_split[0]
anchor = this_split[1]
try:
chapters_with_split_content[chapter].append(anchor)
except KeyError:
chapters_with_split_content[chapter] = []
chapters_with_split_content[chapter].append(anchor)
self.parse_split_chapters(chapters_with_split_content)
# Now we iterate over the ToC as presented in the toc.ncx
# and add chapters to the content list
# In case a split chapter is encountered, get its content
# from the split_chapters dictionary
# What could possibly go wrong?
toc_copy = self.content[:]
# Put the book into the book
for count, i in enumerate(toc_copy):
chapter_file = i[2]
# Get split content according to its corresponding id attribute
if '#' in chapter_file:
this_split = chapter_file.split('#')
chapter_file_proper = this_split[0]
this_anchor = this_split[1]
try:
chapter_content = (
self.split_chapters[chapter_file_proper][this_anchor])
except KeyError:
chapter_content = 'Parse Error'
error_string = (
f'Error parsing {self.book_filename}: {chapter_file_proper}')
logger.error(error_string)
# Get content that remained at the end of the pillaging above
elif chapter_file in self.split_chapters.keys():
try:
chapter_content = self.split_chapters[chapter_file]['top_level']
except KeyError:
chapter_content = 'Parse Error'
error_string = (
f'Error parsing {self.book_filename}: {chapter_file}')
logger.error(error_string)
# Vanilla non split chapters
else:
chapter_content = self.get_chapter_content(chapter_file)
self.content[count][2] = chapter_content
# Cleanup content by removing null chapters
unnamed_chapter_title = 1
content_copy = []
for i in self.content:
if i[2]:
chapter_title = i[1]
if not chapter_title:
chapter_title = unnamed_chapter_title
content_copy.append((
i[0], str(chapter_title), i[2]))
unnamed_chapter_title += 1
self.content = content_copy
# Get cover image and put it in its place
# I imagine this involves saying nasty things to it
# There's no point shifting this to the parser
# The performance increase is negligible
cover_image = self.generate_book_cover()
if cover_image:
cover_path = os.path.join(
self.temp_dir, os.path.basename(self.book_filename)) + ' - cover'
with open(cover_path, 'wb') as cover_temp:
cover_temp.write(cover_image)
# This is probably stupid, but I can't stand the idea of
# having to look at two book covers
cover_replacement_conditions = (
self.cover_image_name.lower() + '.jpg' in self.content[0][2].lower(),
self.cover_image_name.lower() + '.png' in self.content[0][2].lower(),
'cover' in self.content[0][1].lower())
if True in cover_replacement_conditions:
logger.info(
f'Replacing cover {cover_replacement_conditions}: {self.book_filename}')
self.content[0] = (
1, 'Cover',
f'<center><img src="{cover_path}" alt="Cover"></center>')
else:
logger.info('Adding cover: ' + self.book_filename)
self.content.insert(
0,
(1, 'Cover',
f'<center><img src="{cover_path}" alt="Cover"></center>'))
def generate_metadata(self):
book_metadata = self.opf_dict['package']['metadata']
def flattener(this_object):
if isinstance(this_object, collections.OrderedDict):
return this_object['#text']
if isinstance(this_object, list):
if isinstance(this_object[0], collections.OrderedDict):
return this_object[0]['#text']
else:
return this_object[0]
if isinstance(this_object, str):
return this_object
# There are no exception types specified below
# This is on purpose and makes me long for the days
# of simpler, happier things.
# Book title
try:
title = flattener(book_metadata['dc:title'])
except:
logger.warning('Title not found: ' + self.book_filename)
title = os.path.splitext(
os.path.basename(self.book_filename))[0]
# Book author
try:
author = flattener(book_metadata['dc:creator'])
except:
logger.warning('Author not found: ' + self.book_filename)
author = 'Unknown'
# Book year
try:
year = int(flattener(book_metadata['dc:date'])[:4])
except:
logger.warning('Year not found: ' + self.book_filename)
year = 9999
# Book isbn
# Both one and multiple schema
isbn = None
try:
scheme = book_metadata['dc:identifier']['@opf:scheme'].lower()
if scheme.lower() == 'isbn':
isbn = book_metadata['dc:identifier']['#text']
except (TypeError, KeyError):
try:
for i in book_metadata['dc:identifier']:
if i['@opf:scheme'].lower() == 'isbn':
isbn = i['#text']
break
except:
logger.warning('ISBN not found: ' + self.book_filename)
# Book tags
try:
tags = book_metadata['dc:subject']
if isinstance(tags, str):
tags = [tags]
except:
tags = []
# Book cover
cover = self.generate_book_cover()
# Named tuple? Named tuple.
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
self.metadata = Metadata(title, author, year, isbn, tags, cover)
def generate_book_cover(self):
# This is separate because the book cover needs to
# be found and extracted both during addition / reading
book_cover = None
try:
cover_image = [
i['@href'] for i in self.opf_dict['package']['manifest']['item']
if i['@media-type'].split('/')[0] == 'image' and
'cover' in i['@id']][0]
book_cover = self.zip_file.read(self.find_file(cover_image))
except:
logger.warning('Cover not found in opf: ' + self.book_filename)
# Find book cover the hard way
if not book_cover:
biggest_image_size = 0
cover_image = None
for j in self.zip_file.filelist:
if os.path.splitext(j.filename)[1] in ['.jpg', '.jpeg', '.png', '.gif']:
if j.file_size > biggest_image_size:
cover_image = j.filename
biggest_image_size = j.file_size
if cover_image:
book_cover = self.zip_file.read(
self.find_file(cover_image))
if not book_cover:
self.cover_image_name = ''
logger.warning('Cover not found: ' + self.book_filename)
else:
self.cover_image_name = os.path.splitext(
os.path.basename(cover_image))[0]
return book_cover

176
lector/readers/read_fb2.py Normal file
View File

@@ -0,0 +1,176 @@
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import base64
import zipfile
import logging
import collections
from bs4 import BeautifulSoup
logger = logging.getLogger(__name__)
class FB2:
def __init__(self, filename):
self.filename = filename
self.zip_file = None
self.xml = None
self.metadata = None
self.content = []
self.generate_references()
def generate_references(self):
if self.filename.endswith('.fb2.zip'):
this_book = zipfile.ZipFile(
self.filename, mode='r', allowZip64=True)
for i in this_book.filelist:
if os.path.splitext(i.filename)[1] == '.fb2':
book_text = this_book.read(i.filename)
break
else:
with open(self.filename, 'r') as book_file:
book_text = book_file.read()
self.xml = BeautifulSoup(book_text, 'lxml')
def generate_metadata(self):
# All metadata can be parsed in one pass
all_tags = self.xml.find('description')
title = all_tags.find('book-title').text
if title == '' or title is None:
title = os.path.splitext(
os.path.basename(self.filename))[0]
author = all_tags.find(
'author').getText(separator=' ').replace('\n', ' ')
if author == '' or author is None:
author = '<Unknown>'
else:
author = author.strip()
# TODO
# Account for other date formats
try:
year = int(all_tags.find('date').text)
except ValueError:
year = 9999
isbn = None
tags = None
cover = self.generate_book_cover()
Metadata = collections.namedtuple(
'Metadata', ['title', 'author', 'year', 'isbn', 'tags', 'cover'])
self.metadata = Metadata(title, author, year, isbn, tags, cover)
def generate_content(self, temp_dir):
# TODO
# Check what's up with recursion levels
# Why is the TypeError happening in get_title
def get_title(element):
this_title = '<No title>'
title_xml = '<No title xml>'
try:
for i in element:
if i.name == 'title':
this_title = i.getText(separator=' ')
this_title = this_title.replace('\n', '').strip()
title_xml = str(i.unwrap())
break
except TypeError:
return None, None
return this_title, title_xml
def recursor(level, element):
children = element.findChildren('section', recursive=False)
if not children and level != 1:
this_title, title_xml = get_title(element)
self.content.append(
[level, this_title, title_xml + str(element)])
else:
for i in children:
recursor(level + 1, i)
first_element = self.xml.find('section') # Recursive find
siblings = list(first_element.findNextSiblings('section', recursive=False))
siblings.insert(0, first_element)
for this_element in siblings:
this_title, title_xml = get_title(this_element)
# Do not add chapter content in case it has sections
# inside it. This prevents having large Book sections that
# have duplicated content
section_children = this_element.findChildren('section')
chapter_text = str(this_element)
if section_children:
chapter_text = this_title
self.content.append([1, this_title, chapter_text])
recursor(1, this_element)
# Extract all images to the temp_dir
for i in self.xml.find_all('binary'):
image_name = i.get('id')
image_path = os.path.join(temp_dir, image_name)
image_string = f'<image l:href="#{image_name}"'
replacement_string = f'<p></p><img src=\"{image_path}\"'
for j in self.content:
j[2] = j[2].replace(
image_string, replacement_string)
try:
image_data = base64.decodebytes(i.text.encode())
with open(image_path, 'wb') as outimage:
outimage.write(image_data)
except AttributeError:
pass
# Insert the book cover at the beginning
cover_image = self.generate_book_cover()
if cover_image:
cover_path = os.path.join(
temp_dir, os.path.basename(self.filename)) + ' - cover'
with open(cover_path, 'wb') as cover_temp:
cover_temp.write(cover_image)
self.content.insert(
0, (1, 'Cover', f'<center><img src="{cover_path}" alt="Cover"></center>'))
def generate_book_cover(self):
cover = None
try:
cover_image_xml = self.xml.find('coverpage')
for i in cover_image_xml:
cover_image_name = i.get('l:href')
cover_image_data = self.xml.find_all('binary')
for i in cover_image_data:
if cover_image_name.endswith(i.get('id')):
cover = base64.decodebytes(i.text.encode())
except (AttributeError, TypeError):
# Catch TypeError in case no images exist in the book
logger.warning('Cover not found: ' + self.filename)
return cover

View File

@@ -10,5 +10,7 @@
<p>Author: BasioMeusPuga <a href="mailto:disgruntled.mob@gmail.com">disgruntled.mob@gmail.com</a></p> <p>Author: BasioMeusPuga <a href="mailto:disgruntled.mob@gmail.com">disgruntled.mob@gmail.com</a></p>
<p>Page:&nbsp;<a href="https://github.com/BasioMeusPuga/Lector">https://github.com/BasioMeusPuga/Lector</a></p> <p>Page:&nbsp;<a href="https://github.com/BasioMeusPuga/Lector">https://github.com/BasioMeusPuga/Lector</a></p>
<p>License: GPLv3&nbsp;<a href="https://www.gnu.org/licenses/gpl-3.0.en.html">https://www.gnu.org/licenses/gpl-3.0.en.html</a></p> <p>License: GPLv3&nbsp;<a href="https://www.gnu.org/licenses/gpl-3.0.en.html">https://www.gnu.org/licenses/gpl-3.0.en.html</a></p>
<p>Donate (Paypal): <a href="https://www.paypal.me/supportlector">https://www.paypal.me/supportlector</p>
<p>Donate (Bitcoin): 17jaxj26vFJNqQ2hEVerbBV5fpTusfqFro</p>
<p>&nbsp;</p></body> <p>&nbsp;</p></body>
</html> </html>

View File

@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'raw/annotations.ui'
#
# Created by: PyQt5 UI code generator 5.10.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(306, 387)
self.gridLayout = QtWidgets.QGridLayout(Dialog)
self.gridLayout.setObjectName("gridLayout")
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.nameEdit = QtWidgets.QLineEdit(Dialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.nameEdit.sizePolicy().hasHeightForWidth())
self.nameEdit.setSizePolicy(sizePolicy)
self.nameEdit.setObjectName("nameEdit")
self.horizontalLayout_2.addWidget(self.nameEdit)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.typeLabel = QtWidgets.QLabel(Dialog)
self.typeLabel.setObjectName("typeLabel")
self.horizontalLayout.addWidget(self.typeLabel)
self.typeBox = QtWidgets.QComboBox(Dialog)
self.typeBox.setObjectName("typeBox")
self.horizontalLayout.addWidget(self.typeBox)
self.verticalLayout.addLayout(self.horizontalLayout)
self.stackedWidget = QtWidgets.QStackedWidget(Dialog)
self.stackedWidget.setObjectName("stackedWidget")
self.page = QtWidgets.QWidget()
self.page.setObjectName("page")
self.gridLayout_2 = QtWidgets.QGridLayout(self.page)
self.gridLayout_2.setObjectName("gridLayout_2")
self.verticalLayout_12 = QtWidgets.QVBoxLayout()
self.verticalLayout_12.setObjectName("verticalLayout_12")
self.horizontalLayout_15 = QtWidgets.QHBoxLayout()
self.horizontalLayout_15.setObjectName("horizontalLayout_15")
self.foregroundCheck = QtWidgets.QCheckBox(self.page)
self.foregroundCheck.setObjectName("foregroundCheck")
self.horizontalLayout_15.addWidget(self.foregroundCheck)
self.foregroundColorButton = QtWidgets.QPushButton(self.page)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.foregroundColorButton.sizePolicy().hasHeightForWidth())
self.foregroundColorButton.setSizePolicy(sizePolicy)
self.foregroundColorButton.setMinimumSize(QtCore.QSize(30, 0))
self.foregroundColorButton.setMaximumSize(QtCore.QSize(45, 40))
self.foregroundColorButton.setText("")
self.foregroundColorButton.setObjectName("foregroundColorButton")
self.horizontalLayout_15.addWidget(self.foregroundColorButton)
self.verticalLayout_12.addLayout(self.horizontalLayout_15)
self.horizontalLayout_16 = QtWidgets.QHBoxLayout()
self.horizontalLayout_16.setObjectName("horizontalLayout_16")
self.highlightCheck = QtWidgets.QCheckBox(self.page)
self.highlightCheck.setObjectName("highlightCheck")
self.horizontalLayout_16.addWidget(self.highlightCheck)
self.highlightColorButton = QtWidgets.QPushButton(self.page)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.highlightColorButton.sizePolicy().hasHeightForWidth())
self.highlightColorButton.setSizePolicy(sizePolicy)
self.highlightColorButton.setMinimumSize(QtCore.QSize(30, 24))
self.highlightColorButton.setMaximumSize(QtCore.QSize(45, 40))
self.highlightColorButton.setText("")
self.highlightColorButton.setObjectName("highlightColorButton")
self.horizontalLayout_16.addWidget(self.highlightColorButton)
self.verticalLayout_12.addLayout(self.horizontalLayout_16)
self.horizontalLayout_17 = QtWidgets.QHBoxLayout()
self.horizontalLayout_17.setObjectName("horizontalLayout_17")
self.boldCheck = QtWidgets.QCheckBox(self.page)
self.boldCheck.setObjectName("boldCheck")
self.horizontalLayout_17.addWidget(self.boldCheck)
self.verticalLayout_12.addLayout(self.horizontalLayout_17)
self.horizontalLayout_18 = QtWidgets.QHBoxLayout()
self.horizontalLayout_18.setObjectName("horizontalLayout_18")
self.italicCheck = QtWidgets.QCheckBox(self.page)
self.italicCheck.setObjectName("italicCheck")
self.horizontalLayout_18.addWidget(self.italicCheck)
self.verticalLayout_12.addLayout(self.horizontalLayout_18)
self.horizontalLayout_19 = QtWidgets.QHBoxLayout()
self.horizontalLayout_19.setObjectName("horizontalLayout_19")
self.underlineCheck = QtWidgets.QCheckBox(self.page)
self.underlineCheck.setObjectName("underlineCheck")
self.horizontalLayout_19.addWidget(self.underlineCheck)
self.underlineType = QtWidgets.QComboBox(self.page)
self.underlineType.setObjectName("underlineType")
self.horizontalLayout_19.addWidget(self.underlineType)
self.underlineColorButton = QtWidgets.QPushButton(self.page)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.underlineColorButton.sizePolicy().hasHeightForWidth())
self.underlineColorButton.setSizePolicy(sizePolicy)
self.underlineColorButton.setMinimumSize(QtCore.QSize(45, 24))
self.underlineColorButton.setMaximumSize(QtCore.QSize(45, 40))
self.underlineColorButton.setText("")
self.underlineColorButton.setObjectName("underlineColorButton")
self.horizontalLayout_19.addWidget(self.underlineColorButton)
self.verticalLayout_12.addLayout(self.horizontalLayout_19)
self.gridLayout_2.addLayout(self.verticalLayout_12, 0, 0, 1, 1)
self.stackedWidget.addWidget(self.page)
self.verticalLayout.addWidget(self.stackedWidget)
self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1)
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_3.addItem(spacerItem)
self.okButton = QtWidgets.QPushButton(Dialog)
self.okButton.setObjectName("okButton")
self.horizontalLayout_3.addWidget(self.okButton)
self.cancelButton = QtWidgets.QPushButton(Dialog)
self.cancelButton.setObjectName("cancelButton")
self.horizontalLayout_3.addWidget(self.cancelButton)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_3.addItem(spacerItem1)
self.gridLayout.addLayout(self.horizontalLayout_3, 1, 0, 1, 1)
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Annotation Editor"))
self.nameEdit.setPlaceholderText(_translate("Dialog", "Annotation Name"))
self.typeLabel.setText(_translate("Dialog", "Type"))
self.foregroundCheck.setText(_translate("Dialog", "Foreground"))
self.highlightCheck.setText(_translate("Dialog", "Highlight"))
self.boldCheck.setText(_translate("Dialog", "Bold"))
self.italicCheck.setText(_translate("Dialog", "Italic"))
self.underlineCheck.setText(_translate("Dialog", "Underline"))
self.okButton.setText(_translate("Dialog", "OK"))
self.cancelButton.setText(_translate("Dialog", "Cancel"))

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'raw/main.ui' # Form implementation generated from reading ui file 'raw/main.ui'
# #
# Created by: PyQt5 UI code generator 5.9.2 # Created by: PyQt5 UI code generator 5.10.1
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@@ -35,20 +35,6 @@ class Ui_MainWindow(object):
self.gridLayout_4.setContentsMargins(0, 0, 0, 0) self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
self.gridLayout_4.setSpacing(0) self.gridLayout_4.setSpacing(0)
self.gridLayout_4.setObjectName("gridLayout_4") self.gridLayout_4.setObjectName("gridLayout_4")
self.listView = QtWidgets.QListView(self.listPage)
self.listView.setFrameShape(QtWidgets.QFrame.NoFrame)
self.listView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.listView.setProperty("showDropIndicator", False)
self.listView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.listView.setMovement(QtWidgets.QListView.Static)
self.listView.setProperty("isWrapping", True)
self.listView.setResizeMode(QtWidgets.QListView.Fixed)
self.listView.setLayoutMode(QtWidgets.QListView.SinglePass)
self.listView.setViewMode(QtWidgets.QListView.IconMode)
self.listView.setUniformItemSizes(True)
self.listView.setWordWrap(True)
self.listView.setObjectName("listView")
self.gridLayout_4.addWidget(self.listView, 0, 0, 1, 1)
self.stackedWidget.addWidget(self.listPage) self.stackedWidget.addWidget(self.listPage)
self.tablePage = QtWidgets.QWidget() self.tablePage = QtWidgets.QWidget()
self.tablePage.setObjectName("tablePage") self.tablePage.setObjectName("tablePage")
@@ -56,20 +42,6 @@ class Ui_MainWindow(object):
self.gridLayout_3.setContentsMargins(0, 0, 0, 0) self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
self.gridLayout_3.setSpacing(0) self.gridLayout_3.setSpacing(0)
self.gridLayout_3.setObjectName("gridLayout_3") self.gridLayout_3.setObjectName("gridLayout_3")
self.tableView = QtWidgets.QTableView(self.tablePage)
self.tableView.setFrameShape(QtWidgets.QFrame.Box)
self.tableView.setFrameShadow(QtWidgets.QFrame.Plain)
self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContentsOnFirstShow)
self.tableView.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed|QtWidgets.QAbstractItemView.SelectedClicked)
self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.tableView.setGridStyle(QtCore.Qt.NoPen)
self.tableView.setSortingEnabled(True)
self.tableView.setWordWrap(False)
self.tableView.setObjectName("tableView")
self.tableView.horizontalHeader().setVisible(True)
self.tableView.verticalHeader().setVisible(False)
self.gridLayout_3.addWidget(self.tableView, 0, 0, 1, 1)
self.stackedWidget.addWidget(self.tablePage) self.stackedWidget.addWidget(self.tablePage)
self.gridLayout_2.addWidget(self.stackedWidget, 0, 0, 1, 1) self.gridLayout_2.addWidget(self.stackedWidget, 0, 0, 1, 1)
self.tabWidget.addTab(self.tab, "") self.tabWidget.addTab(self.tab, "")

View File

@@ -94,26 +94,26 @@ def generate_pie(progress_percent, temp_dir=None):
return lSvg return lSvg
def pixmapper(current_chapter, total_chapters, temp_dir, size): def pixmapper(position_percent, temp_dir, consider_read_at, size):
# A current_chapter of -1 implies the files does not exist # A current_chapter of -1 implies the files does not exist
# A chapter number == Total chapters implies the file is unread # position_percent and consider_read_at are expected as a <1 decimal value
return_pixmap = None
if current_chapter == -1: return_pixmap = None
consider_read_at = consider_read_at / 100
if position_percent == -1:
return_pixmap = QtGui.QIcon(':/images/error.svg').pixmap(size) return_pixmap = QtGui.QIcon(':/images/error.svg').pixmap(size)
return return_pixmap return return_pixmap
if current_chapter == total_chapters: if position_percent >= consider_read_at: # Consider book read @ this progress
return_pixmap = QtGui.QIcon(':/images/checkmark.svg').pixmap(size) return_pixmap = QtGui.QIcon(':/images/checkmark.svg').pixmap(size)
else: else:
# TODO # TODO
# See if saving the svg to disk can be avoided # See if saving the svg to disk can be avoided
# Shift to lines to track progress
# Maybe make the alignment a little more uniform across emblems # Maybe make the alignment a little more uniform across emblems
progress_percent = int(current_chapter * 100 / total_chapters) generate_pie(int(position_percent * 100), temp_dir)
generate_pie(progress_percent, temp_dir)
svg_path = os.path.join(temp_dir, 'lector_progress.svg') svg_path = os.path.join(temp_dir, 'lector_progress.svg')
return_pixmap = QtGui.QIcon(svg_path).pixmap(size - 4) ## The -4 looks more proportional return_pixmap = QtGui.QIcon(svg_path).pixmap(size - 4) ## The -4 looks more proportional

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 1.0039062 C 4.134 1.0039062 1 4.1380063 1 8.0039062 C 1 11.869906 4.134 15.003906 8 15.003906 C 11.866 15.003906 15 11.869906 15 8.0039062 C 15 4.1380063 11.866 1.0039062 8 1.0039062 z M 8 3.7539062 C 8.69036 3.7539062 9.25 4.3135463 9.25 5.0039062 C 9.25 5.6942662 8.69036 6.2539062 8 6.2539062 C 7.30964 6.2539062 6.75 5.6942662 6.75 5.0039062 C 6.75 4.3135463 7.30964 3.7539062 8 3.7539062 z M 7 7.0039062 L 9 7.0039062 L 9 12.003906 L 7 12.003906 L 7 7.0039062 z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 815 B

View File

Before

Width:  |  Height:  |  Size: 428 B

After

Width:  |  Height:  |  Size: 428 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="m12.213 1c-0.213 0-0.425 0.083-0.59 0.248l-1.6308 1.6387 3.1208 3.1211 1.639-1.6308c0.33-0.33 0.33-0.8497 0-1.1797l-1.949-1.9493c-0.165-0.165-0.378-0.248-0.59-0.248zm-3.34 3.0078l-7.8808 7.8792-0.00001 3.121h3.1211l0.0078-0.008h10.879v-2h-8.8789l5.8709-5.873-3.119-3.1192z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 3 6 L 8 11 L 13 6 L 3 6 z" transform="translate(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8 5 L 3 10 L 13 10 L 8 5 z" transform="translate(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 703 B

View File

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 546 B

View File

Before

Width:  |  Height:  |  Size: 891 B

After

Width:  |  Height:  |  Size: 891 B

View File

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 729 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 5.9980469 1.0195312 L 5.9980469 7.0195312 L 3.6582031 7.0195312 L 7.9902344 13.324219 L 12.371094 7.0195312 L 9.9980469 7.0195312 L 9.9980469 1.0488281 L 5.9980469 1.0195312 z M 1 14.03125 L 1 16 L 15.005859 16 L 15 14.03125 L 1 14.03125 z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 586 B

View File

Before

Width:  |  Height:  |  Size: 561 B

After

Width:  |  Height:  |  Size: 561 B

View File

Before

Width:  |  Height:  |  Size: 601 B

After

Width:  |  Height:  |  Size: 601 B

View File

Before

Width:  |  Height:  |  Size: 565 B

After

Width:  |  Height:  |  Size: 565 B

View File

Before

Width:  |  Height:  |  Size: 565 B

After

Width:  |  Height:  |  Size: 565 B

View File

Before

Width:  |  Height:  |  Size: 561 B

After

Width:  |  Height:  |  Size: 561 B

View File

Before

Width:  |  Height:  |  Size: 565 B

After

Width:  |  Height:  |  Size: 565 B

View File

Before

Width:  |  Height:  |  Size: 484 B

After

Width:  |  Height:  |  Size: 484 B

View File

Before

Width:  |  Height:  |  Size: 484 B

After

Width:  |  Height:  |  Size: 484 B

View File

Before

Width:  |  Height:  |  Size: 694 B

After

Width:  |  Height:  |  Size: 694 B

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="22"
height="22"
viewBox="0 0 22 22"
version="1.1"
id="svg7"
sodipodi:docname="invert.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<metadata
id="metadata11">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="855"
inkscape:window-height="480"
id="namedview9"
showgrid="false"
inkscape:zoom="10.727273"
inkscape:cx="11"
inkscape:cy="11"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="current-color-scheme"
type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path
style="fill:#5c616c;fill-opacity:1"
class="ColorScheme-Text"
d="M 2 1 L 2 15 L 9 15 L 9 13 A 5 5 0 0 1 4 8 A 5 5 0 0 1 9 3 L 9 1 L 2 1 z M 9 3 L 9 13 C 11.7614 13 14 10.7614 14 8 C 14 5.2386 11.7614 3 9 3 z"
transform="translate(3 3)"
id="path5" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 642 B

After

Width:  |  Height:  |  Size: 642 B

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="22"
height="22"
version="1.1"
viewBox="0 0 22 22"
id="svg7"
sodipodi:docname="manga.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata11">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1586"
inkscape:window-height="856"
id="namedview9"
showgrid="false"
inkscape:zoom="10.727273"
inkscape:cx="11"
inkscape:cy="11"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="current-color-scheme"
type="text/css">
.ColorScheme-Text { color:#6e6e6e; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path
style="fill:#5c616c;fill-opacity:1"
class="ColorScheme-Text"
d="M 19,11 14,6 V 8 H 8 V 6 l -5,5 5,5 v -2 h 6 v 2 z"
id="path5" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="22"
height="22"
viewBox="0 0 22 22"
version="1.1"
id="svg7"
sodipodi:docname="page-double.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<metadata
id="metadata11">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1043"
id="namedview9"
showgrid="false"
inkscape:zoom="10.727273"
inkscape:cx="11"
inkscape:cy="11"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="current-color-scheme"
type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; }
</style>
</defs>
<path
style="fill:#5c616c;fill-opacity:1"
class="ColorScheme-Text"
d="M 1 1 L 1 15 L 15 15 L 15 1 L 1 1 z M 3 3 L 7 3 L 7 13 L 3 13 L 3 3 z M 9 3 L 13 3 L 13 13 L 9 13 L 9 3 z"
transform="translate(3 3)"
id="path5" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="22"
height="22"
viewBox="0 0 22 22"
version="1.1"
id="svg7"
sodipodi:docname="page-flow.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<metadata
id="metadata11">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1043"
id="namedview9"
showgrid="false"
inkscape:zoom="15.170655"
inkscape:cx="-1.2825494"
inkscape:cy="5.330307"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="current-color-scheme"
type="text/css">
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; }
</style>
</defs>
<path
style="fill:#5c616c;fill-opacity:1"
class="ColorScheme-Text"
d="M 2 1 L 2 15 L 14 15 L 14 1 L 2 1 z M 4 3 L 12 3 L 12 13 L 4 13 L 4 3 z"
transform="translate(3 3)"
id="path5" />
<g
transform="matrix(0.64844409,0,0,0.64844409,5.8379769,5.8670545)"
id="g7"
style="fill:#5c616c;fill-opacity:1">
<path
inkscape:connector-curvature="0"
class="ColorScheme-Text"
d="m 7,2 v 8 L 3.5,6.5 2,8 8,14 14,8 12.5,6.5 9,10 V 2 Z"
style="color:#dfdfdf;fill:#5c616c;fill-opacity:1"
id="path5-3" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 2 1 L 2 15 L 14 15 L 14 1 L 2 1 z M 4 3 L 12 3 L 12 13 L 4 13 L 4 3 z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

View File

Before

Width:  |  Height:  |  Size: 859 B

After

Width:  |  Height:  |  Size: 859 B

View File

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 378 B

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="22"
height="22"
viewBox="0 0 22 22"
version="1.1"
id="svg7"
sodipodi:docname="rotate-left.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<metadata
id="metadata11">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="855"
inkscape:window-height="480"
id="namedview9"
showgrid="false"
inkscape:zoom="10.727273"
inkscape:cx="11"
inkscape:cy="11"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="current-color-scheme"
type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; }
</style>
</defs>
<path
style="fill:#5c616c;fill-opacity:1"
class="ColorScheme-Text"
d="M 8.0292969 0.001953125 L 4.0292969 3.0019531 L 8.0292969 6.0019531 L 8.0292969 4.0019531 C 10.238397 4.0019531 12.029297 5.7928531 12.029297 8.0019531 C 12.029297 10.211053 10.238397 12.001953 8.0292969 12.001953 C 5.8201969 12.001953 4.0292969 10.211053 4.0292969 8.0019531 A 1 1 0 0 0 3.0292969 7.0019531 A 1 1 0 0 0 2.0292969 8.0019531 A 1 1 0 0 0 2.0351562 8.1015625 C 2.0889563 11.368762 4.7491969 14.001953 8.0292969 14.001953 C 11.342997 14.001953 14.029297 11.315653 14.029297 8.0019531 C 14.029297 4.6882531 11.342997 2.0019531 8.0292969 2.0019531 L 8.0292969 0.001953125 z"
transform="translate(3 3)"
id="path5" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="22"
height="22"
viewBox="0 0 22 22"
version="1.1"
id="svg7"
sodipodi:docname="rotate-right.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<metadata
id="metadata11">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="855"
inkscape:window-height="480"
id="namedview9"
showgrid="false"
inkscape:zoom="10.727273"
inkscape:cx="11"
inkscape:cy="11"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="current-color-scheme"
type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; }
</style>
</defs>
<path
style="fill:#5c616c;fill-opacity:1"
class="ColorScheme-Text"
d="M 7.9804688 0.001953125 L 7.9804688 2.0019531 C 4.6667688 2.0019531 1.9804688 4.6882531 1.9804688 8.0019531 C 1.9804688 11.315653 4.6667688 14.001953 7.9804688 14.001953 C 11.260569 14.001953 13.920809 11.368762 13.974609 8.1015625 A 1 1 0 0 0 13.980469 8.0019531 A 1 1 0 0 0 12.980469 7.0019531 A 1 1 0 0 0 11.980469 8.0019531 C 11.980469 10.211053 10.189569 12.001953 7.9804688 12.001953 C 5.7713688 12.001953 3.9804688 10.211053 3.9804688 8.0019531 C 3.9804688 5.7928531 5.7713688 4.0019531 7.9804688 4.0019531 L 7.9804688 6.0019531 L 11.980469 3.0019531 L 7.9804688 0.001953125 z"
transform="translate(3 3)"
id="path5" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
version="1.1"
id="svg4"
sodipodi:docname="search-case.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1480"
inkscape:window-height="750"
id="namedview6"
showgrid="false"
inkscape:zoom="14.75"
inkscape:cx="-6.5084746"
inkscape:cy="8"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<path
style="fill:#5c616c;fill-opacity:1"
d="M 4.7890625,2 1,13 h 1.90625 l 0.6796875,-2 h 3.8300781 l 0.6875,2 H 10 L 6.2109375,2 Z M 5.4980469,5.4375 6.6835938,9 H 4.3144531 Z M 11.5,8 v 1 h 3 C 14.715,9 15,9.305 15,9.5 V 10 h -2.5 c -0.46,0 -0.87,0.189375 -1.125,0.484375 C 11.12,10.774375 11,11.14 11,11.5 c 0,0.36 0.135625,0.725625 0.390625,1.015625 C 11.645625,12.805625 12.045,13 12.5,13 H 16 V 9.5 C 16,8.685 15.34,8 14.5,8 Z m 1,3 H 15 v 1 H 12.5 C 12.3,12 12.215625,11.944375 12.140625,11.859375 12.065625,11.774375 12,11.64 12,11.5 12,11.36 12.05,11.225625 12.125,11.140625 12.2,11.060625 12.29,11 12.5,11 Z"
id="path2" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
version="1.1"
id="svg4"
sodipodi:docname="search-word.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1043"
id="namedview6"
showgrid="false"
inkscape:zoom="45.254834"
inkscape:cx="6.5123537"
inkscape:cy="6.8917559"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
style="fill:#5c616c;fill-opacity:1"
d="M 3,3 C 1.02036,3 -9.9999927e-7,4.1718311 0,6.6074219 V 8.5 A 1.5,1.5 0 0 0 1.5,10 1.5,1.5 0 0 0 3,8.5 1.5,1.5 0 0 0 1.5097656,7 l -0.00781,-0.4023438 c 0,-0.948207 0.1116447,-1.5979182 0.3378907,-1.9511718 C 2.0660618,4.29323 2.453243,4.0371834 3,4 Z M 7,3 C 5.02036,3 3.999999,4.1718311 4,6.6074219 V 8.5 A 1.5,1.5 0 0 0 5.5,10 1.5,1.5 0 0 0 7,8.5 1.5,1.5 0 0 0 5.5097656,7 l -0.00781,-0.4023438 c 0,-0.948207 0.1116447,-1.5979182 0.3378907,-1.9511718 C 6.0660608,4.29323 6.453243,4.0371834 7,4 Z M 9.5,5 A 1.5,1.5 0 0 0 8,6.5 1.5,1.5 0 0 0 9.490234,8 l 0.00781,0.4023438 c 0,0.948207 -0.111645,1.5979182 -0.337891,1.9511722 C 8.9339389,10.70677 8.5467573,10.962817 8,11 v 1 c 1.97964,0 3.000001,-1.171831 3,-3.6074219 V 6.5 A 1.5,1.5 0 0 0 9.5,5 Z m 4,0 A 1.5,1.5 0 0 0 12,6.5 1.5,1.5 0 0 0 13.490234,8 l 0.0078,0.4023438 c 0,0.948207 -0.111645,1.5979182 -0.337891,1.9511722 C 12.933938,10.70677 12.546757,10.962817 12,11 v 1 c 1.97964,0 3.000001,-1.171831 3,-3.6074219 V 6.5 A 1.5,1.5 0 0 0 13.5,5 Z"
id="path2" />
<path
style="fill:#5c616c;stroke:none;stroke-width:0.06779661;fill-opacity:1"
d="M 8.040678,11.489133 V 11.01394 l 0.2542373,-0.0411 C 8.6241141,10.91962 9.0049959,10.657098 9.1919904,10.354534 9.3774432,10.054466 9.5267422,9.2371719 9.5298381,8.5050867 9.5321781,7.9511596 9.5305475,7.9457647 9.3605343,7.9457647 8.6870054,7.9457647 8.0454876,7.2386514 8.0421669,6.4925935 8.0367069,5.2649491 9.4833138,4.5924333 10.437555,5.379 c 0.470567,0.3878808 0.518377,0.5947737 0.518377,2.2432227 0,0.9924588 -0.03143,1.6284257 -0.09756,1.9740504 -0.267079,1.3958959 -1.1175811,2.2244799 -2.3939662,2.3322709 l -0.4237288,0.03579 z"
id="path4524"
inkscape:connector-curvature="0" />
<path
style="fill:#5c616c;stroke:none;stroke-width:0.00390625;fill-opacity:1"
d="m 9.7117131,11.513295 c 0.00204,-0.0018 0.023926,-0.01759 0.048633,-0.03507 0.1798996,-0.127248 0.3444879,-0.28336 0.4858319,-0.460812 0.252616,-0.317149 0.444866,-0.723077 0.559095,-1.1805105 0.06615,-0.2648918 0.09348,-0.4533196 0.116987,-0.8066406 C 10.94809,8.6420545 10.9575,8.1721993 10.95441,7.4247931 10.952,6.840388 10.947836,6.672371 10.92997,6.438465 10.924141,6.3621478 10.9106,6.2310896 10.906461,6.2109259 l -0.0022,-0.010742 h 0.0313 c 0.0366,0 0.0324,-0.00607 0.0433,0.0625 0.01847,0.1161553 0.01782,0.070812 0.01782,1.25 0,1.1705155 -1.65e-4,1.1838055 -0.01779,1.4296875 -0.05607,0.7822203 -0.232739,1.4031726 -0.538111,1.8913516 -0.157745,0.252177 -0.36026,0.471333 -0.5979529,0.647089 l -0.048386,0.03578 h -0.043215 c -0.023798,0 -0.041548,-0.0015 -0.039504,-0.0033 z"
id="path4526"
inkscape:connector-curvature="0" />
<path
style="fill:#5c616c;stroke:none;stroke-width:0.00390625;fill-opacity:1"
d="m 8.0037065,11.500359 v -0.499846 l 0.038086,-0.0025 c 0.052532,-0.0035 0.1567951,-0.0193 0.2234149,-0.03389 C 8.640889,10.881833 8.9448125,10.678717 9.1476483,10.374368 9.3523256,10.067256 9.4641833,9.5521406 9.492672,8.7855008 L 9.497502,8.655618 H 9.512387 9.527272 L 9.524992,8.705423 C 9.5049185,9.1437939 9.4502274,9.5471986 9.3653315,9.8830268 9.3036893,10.126869 9.2445481,10.277176 9.1635619,10.395823 9.0101537,10.620571 8.748788,10.822299 8.4830209,10.92108 c -0.078653,0.02923 -0.1439508,0.04431 -0.30158,0.06961 l -0.140625,0.02258 -9.922e-4,0.475456 c -9.556e-4,0.457983 -7.301e-4,0.475456 0.00614,0.475456 0.012211,0 0.421374,-0.03493 0.483138,-0.04125 0.3949367,-0.04038 0.7679272,-0.159625 1.078125,-0.344663 0.023633,-0.0141 0.0556,-0.03409 0.071038,-0.04442 l 0.028069,-0.01879 0.04029,0.003 0.04029,0.003 -0.046244,0.03035 c -0.2869093,0.188322 -0.6383031,0.321037 -1.0300398,0.389028 -0.189836,0.03295 -0.3718111,0.04969 -0.6219586,0.0572 l -0.084961,0.0026 z"
id="path4528"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 6.4902344 0.99609375 C 3.4613344 0.99609375 0.99023438 3.4706937 0.99023438 6.4960938 C 0.99023438 9.5214938 3.4613344 11.996094 6.4902344 11.996094 C 7.6422344 11.996094 8.7279444 11.638254 9.6152344 11.027344 L 13.302734 14.714844 A 1.0055 1.0055 0 1 0 14.708984 13.277344 L 11.021484 9.5898438 C 11.632274 8.7038438 12.021484 7.6459938 12.021484 6.4960938 C 12.021484 3.4706937 9.5190344 0.99609375 6.4902344 0.99609375 z M 6.4902344 2.9960938 C 8.4376344 2.9960938 9.9902344 4.5508938 9.9902344 6.4960938 C 9.9902344 8.4411937 8.4376344 9.9960938 6.4902344 9.9960938 C 4.5428344 9.9960938 2.9902344 8.4411937 2.9902344 6.4960938 C 2.9902344 4.5508938 4.5428344 2.9960938 6.4902344 2.9960938 z" transform="translate(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 2 0 C 0.892 0 0 0.892 0 2 L 0 14 C 0 15.108 0.892 16 2 16 L 14 16 C 15.108 16 16 15.108 16 14 L 16 2 C 16 0.892 15.108 0 14 0 L 2 0 z M 3.7148438 2 L 12.285156 2 C 13.235156 2 14 2.7651437 14 3.7148438 L 14 12.285156 C 14 13.235156 13.235156 14 12.285156 14 L 3.7148438 14 C 2.7651438 14 2 13.235156 2 12.285156 L 2 3.7148438 C 2 2.7651438 2.7651437 2 3.7148438 2 z M 6.7402344 3 L 6.6289062 4.3164062 A 3.964 3.9286 0 0 0 5.4707031 4.9804688 L 4.2617188 4.4179688 L 3.0019531 6.5820312 L 4.0976562 7.3378906 A 3.964 3.9286 0 0 0 4.0371094 8 A 3.964 3.9286 0 0 0 4.0957031 8.6660156 L 3.0019531 9.4179688 L 4.2617188 11.582031 L 5.4667969 11.019531 A 3.964 3.9286 0 0 0 6.6289062 11.679688 L 6.7402344 13 L 9.2617188 13 L 9.3730469 11.683594 A 3.964 3.9286 0 0 0 10.53125 11.019531 L 11.740234 11.582031 L 13.001953 9.4179688 L 11.904297 8.6621094 A 3.964 3.9286 0 0 0 11.964844 8 A 3.964 3.9286 0 0 0 11.908203 7.3339844 L 13.001953 6.5820312 L 11.740234 4.4179688 L 10.535156 4.9804688 A 3.964 3.9286 0 0 0 9.3730469 4.3203125 L 9.2617188 3 L 6.7402344 3 z M 8.0019531 6.5722656 A 1.4414 1.4286 0 0 1 9.4433594 8 A 1.4414 1.4286 0 0 1 8.0019531 9.4277344 A 1.4414 1.4286 0 0 1 6.5605469 8 A 1.4414 1.4286 0 0 1 8.0019531 6.5722656 z" transform="translate(4 4)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 501 B

After

Width:  |  Height:  |  Size: 501 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#5c616c; } .ColorScheme-Highlight { color:#5294e2; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 1 3.0039062 L 1 5.0039062 L 3 5.0039062 L 3 3.0039062 L 1 3.0039062 z M 5 3.0039062 L 5 5.0039062 L 15 5.0039062 L 15 3.0039062 L 5 3.0039062 z M 1 7.0039062 L 1 9.0039062 L 3 9.0039062 L 3 7.0039062 L 1 7.0039062 z M 5 7.0039062 L 5 9.0039062 L 15 9.0039062 L 15 7.0039062 L 5 7.0039062 z M 1 11.003906 L 1 13.003906 L 3 13.003906 L 3 11.003906 L 1 11.003906 z M 5 11.003906 L 5 13.003906 L 15 13.003906 L 15 11.003906 L 5 11.003906 z" transform="translate(3 3)"/>
</svg>

After

Width:  |  Height:  |  Size: 782 B

View File

Before

Width:  |  Height:  |  Size: 781 B

After

Width:  |  Height:  |  Size: 781 B

View File

Before

Width:  |  Height:  |  Size: 916 B

After

Width:  |  Height:  |  Size: 916 B

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Some files were not shown because too many files have changed in this diff Show More