Compare commits
198 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4389a0f5aa | ||
|
b54ff37828 | ||
|
e7dd10fa3a | ||
|
3ede5b78fa | ||
|
418d9e0c1c | ||
|
e977826ea1 | ||
|
c661ed54de | ||
|
dd3aa8a49c | ||
|
65ad48c442 | ||
|
fd433d6432 | ||
|
2bc73450fe | ||
|
8b6800e14f | ||
|
d0cdd531a9 | ||
|
5e74b6f261 | ||
|
24e45ac2b7 | ||
|
916bdb5b14 | ||
|
ca108da948 | ||
|
6762f2cfce | ||
|
0aea9ec33b | ||
|
af1b988d93 | ||
|
8fd6a0d432 | ||
|
56f15528c2 | ||
|
f358ad169c | ||
|
4cf0a9e78c | ||
|
38de0dcd13 | ||
|
eb49ca92a4 | ||
|
bf93c7beab | ||
|
ca57983739 | ||
|
c71985f621 | ||
|
c8fe0ba8b6 | ||
|
d6df28c503 | ||
|
75ace25c57 | ||
|
d2d7dc2c8f | ||
|
f622b0c23e | ||
|
f312714a2c | ||
|
c3f26ca225 | ||
|
f6c7307647 | ||
|
b1714b9674 | ||
|
fa030e3060 | ||
|
564db06179 | ||
|
3cd75807f9 | ||
|
f6f9d01060 | ||
|
c6e30b67ad | ||
|
e4be239bf0 | ||
|
1e004774c9 | ||
|
91ca1e2190 | ||
|
d1662b47d9 | ||
|
dfe0fceea9 | ||
|
268014cc3a | ||
|
d1b1d7c59c | ||
|
470fc1078f | ||
|
96f4d9193a | ||
|
7aa42603bd | ||
|
9a6392d1e6 | ||
|
739b84e9f4 | ||
|
66746b4eaa | ||
|
164450a888 | ||
|
191ea7ef3a | ||
|
ca8ddd38a2 | ||
|
a45e183914 | ||
|
506c458544 | ||
|
5e3987dc04 | ||
|
5b8bc1d707 | ||
|
b3e4060661 | ||
|
2185e9fcf7 | ||
|
5d35319164 | ||
|
c6d24fd970 | ||
|
17f39c557b | ||
|
f997bc9c9a | ||
|
930a97a8fa | ||
|
026fff3d7a | ||
|
f9eec130dd | ||
|
d75689ea97 | ||
|
6ea5635d28 | ||
|
4b9221128c | ||
|
b5349315be | ||
|
ee18f157f1 | ||
|
ae325736d5 | ||
|
826da72d4b | ||
|
b5231cd383 | ||
|
5ac843e48c | ||
|
74417319be | ||
|
f8555a6ed5 | ||
|
4346c27adc | ||
|
16adf57dae | ||
|
8534088f4a | ||
|
a81ed537a6 | ||
|
ed5bc0b2b9 | ||
|
8cb8904e58 | ||
|
aa093b8cc2 | ||
|
42b4d0317d | ||
|
a0e463bc58 | ||
|
4a2da61b51 | ||
|
5c481ccafe | ||
|
30760b879e | ||
|
af7868f62a | ||
|
62c44730d8 | ||
|
045d8a3e52 | ||
|
55bee210c6 | ||
|
d3746c8e98 | ||
|
ffcf07414f | ||
|
ffaace2eaa | ||
|
32455dd859 | ||
|
3e54340694 | ||
|
bc6c7d1c36 | ||
|
ebd746b7b2 | ||
|
ebc3ef9f1b | ||
|
7238605441 | ||
|
ea86737970 | ||
|
ab4c586c06 | ||
|
7977bde410 | ||
|
626472dd04 | ||
|
d9efe2da3c | ||
|
ec197f0829 | ||
|
335479bcfb | ||
|
cbf01c6d16 | ||
|
98ca118a60 | ||
|
c7aa0e28ee | ||
|
528c2e387c | ||
|
bc54d6b686 | ||
|
8f298de58e | ||
|
366859ebe0 | ||
|
8c51cc047e | ||
|
5081a31f1a | ||
|
aff69d95c1 | ||
|
0b8427c864 | ||
|
43dd6a34d9 | ||
|
2f4adfc183 | ||
|
0d015ad72e | ||
|
406ca0485f | ||
|
ab6760226e | ||
|
66c8626d43 | ||
|
5fa724ae69 | ||
|
d417a94829 | ||
|
9c85a1075e | ||
|
dd4b502861 | ||
|
0f963b20f9 | ||
|
f63b6627b2 | ||
|
00db5d5e0f | ||
|
5e53d40e68 | ||
|
6ffa6934ed | ||
|
34dcf9f1b4 | ||
|
7931f92335 | ||
|
42b655862c | ||
|
e6eb056ec6 | ||
|
7f5b6fc349 | ||
|
9af175b11f | ||
|
c783e44444 | ||
|
a1dba753e8 | ||
|
a55a0e7205 | ||
|
bb8de60efe | ||
|
0d8c2b6648 | ||
|
64a96d816d | ||
|
5a4af54118 | ||
|
4cf18e008d | ||
|
50cc52b116 | ||
|
35f38b9f68 | ||
|
c883ba0175 | ||
|
39cf03a70e | ||
|
44d88d99bb | ||
|
ca67071e91 | ||
|
b5acce6449 | ||
|
7bdf01a67e | ||
|
aca08827fb | ||
|
d4aaa4dc74 | ||
|
98daa40bfd | ||
|
a7df896468 | ||
|
fd149dcafa | ||
|
0bb2e9329f | ||
|
89a32bfeda | ||
|
50089cb57a | ||
|
a62e681223 | ||
|
efe52cd3cb | ||
|
23aff44412 | ||
|
e8e3b81871 | ||
|
03b683e05d | ||
|
5b3759afe6 | ||
|
fc2fcb5361 | ||
|
6ee135a52b | ||
|
55545f62e7 | ||
|
160226c6cd | ||
|
5d3ce17447 | ||
|
03afc6933f | ||
|
c9559daaf6 | ||
|
3c293a39d3 | ||
|
a87cd24c3d | ||
|
8564ede48b | ||
|
e0b20e36dd | ||
|
c2db6c13b0 | ||
|
dbff4cbcca | ||
|
ce9ee4ccb2 | ||
|
ca3d747136 | ||
|
9e5559bbfa | ||
|
9e9d7cca90 | ||
|
05e1655fd9 | ||
|
f9bcc399e8 | ||
|
1cd6ff6b58 | ||
|
79180885b5 |
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
__pycache__/
|
||||||
|
.gitignore
|
||||||
|
.vscode/
|
||||||
|
parsers/__pycache__/
|
||||||
|
books/
|
||||||
|
Examples/
|
@@ -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, e:
|
|
||||||
print "Error: %s" % e
|
|
||||||
return 1
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
40
Lector.pro
Normal 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
|
95
README.md
@@ -1,45 +1,110 @@
|
|||||||
# 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
|
||||||
* epub
|
* epub
|
||||||
|
* djvu
|
||||||
|
* fb2
|
||||||
* mobi
|
* mobi
|
||||||
* azw / azw3 / azw4
|
* azw / azw3 / azw4
|
||||||
* cbr / cbz
|
* cbr / cbz
|
||||||
|
* md
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
[Paypal](https://www.paypal.me/supportlector)
|
||||||
|
|
||||||
|
Bitcoin: 17jaxj26vFJNqQ2hEVerbBV5fpTusfqFro
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
* Qt5
|
### Needed
|
||||||
* PyQt5
|
| Package | Version tested |
|
||||||
* python-requests
|
| --- | --- |
|
||||||
* python-beautifulsoup4
|
| Python | 3.6 |
|
||||||
|
| PyQt5 | 5.10.1 |
|
||||||
|
| python-lxml | 4.3.0 |
|
||||||
|
| python-beautifulsoup4 | 4.6.0 |
|
||||||
|
| python-xmltodict | 0.11.0 |
|
||||||
|
|
||||||
|
### 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
|
||||||
0. Install dependencies
|
### Manual
|
||||||
|
0. Install dependencies - I recommend using your package manager for this.
|
||||||
1. Clone repository
|
1. Clone repository
|
||||||
2. Launch with \_\_main\_\_.py
|
2. Type the following in the root directory:
|
||||||
|
|
||||||
|
$ python setup.py build
|
||||||
|
# python setup.py install
|
||||||
|
3. OR launch with `lector/__main__.py`
|
||||||
|
|
||||||
|
### Available packages
|
||||||
|
* [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)
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
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.
|
||||||
|
|
||||||
|
Please keep the translations short. There's only so much space for UI elements.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
### Main window
|
### Main window
|
||||||

|

|
||||||
|
|
||||||
### Table view
|
### Table view
|
||||||

|

|
||||||
|
|
||||||
### Book reading view
|
### Book reading view
|
||||||

|

|
||||||
|
|
||||||
|
### Distraction free view
|
||||||
|

|
||||||
|
|
||||||
|
### Annotation support
|
||||||
|

|
||||||
|
|
||||||
### Comic reading view
|
### Comic reading view
|
||||||

|

|
||||||
|
|
||||||
### Bookmark support
|
### Bookmark support
|
||||||

|

|
||||||
|
|
||||||
### View profiles
|
### View profiles
|
||||||

|

|
||||||
|
|
||||||
### Metadata editor
|
### Metadata editor
|
||||||

|

|
||||||
|
|
||||||
### In program dictionary
|
### In program dictionary
|
||||||

|

|
||||||
|
|
||||||
|
### Settings window
|
||||||
|

|
||||||
|
|
||||||
|
## Attributions
|
||||||
|
* [KindleUnpack](https://github.com/kevinhendricks/KindleUnpack)
|
||||||
|
* [rarfile](https://github.com/markokr/rarfile)
|
||||||
|
* [Papirus icon theme](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme)
|
||||||
|
87
TODO
@@ -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
|
||||||
@@ -24,9 +30,16 @@ TODO
|
|||||||
✓ Context menu: Cache, Read, Edit database, delete, Mark read/unread
|
✓ Context menu: Cache, Read, Edit database, delete, Mark read/unread
|
||||||
✓ 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
|
||||||
|
✓ 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
|
||||||
@@ -50,46 +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
|
||||||
|
✓ 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
|
||||||
Exiting with Ctrl + Q does not save the cursor position INITIALLY
|
Screen position still keeps jumping when inside a paragraph
|
||||||
|
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
|
txt, doc, chm support
|
||||||
Scrolling: Smooth / By Line
|
|
||||||
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
|
||||||
? pdf support
|
In application notifications
|
||||||
? Create emblem per filetype
|
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
|
||||||
|
143
com.basiomeuspuga.Lector.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -1,276 +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 sys
|
|
||||||
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 = {}
|
|
||||||
|
|
||||||
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:
|
|
||||||
print(str(filename) + ' not found in zip')
|
|
||||||
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):
|
|
||||||
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 conventioanl 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 = unquote(chapter_source.split('#')[0])
|
|
||||||
self.book['navpoint_dict'][chapter_source] = 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 not split_large_xml:
|
|
||||||
try:
|
|
||||||
self.book['book_list'].append(
|
|
||||||
(self.book['navpoint_dict'][i], chapter_data))
|
|
||||||
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'
|
|
||||||
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>')
|
|
||||||
|
|
||||||
else:
|
|
||||||
# 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))
|
|
||||||
no_title_chapter += 1
|
|
||||||
|
|
||||||
def main():
|
|
||||||
book = EPUB(sys.argv[1])
|
|
||||||
book.read_epub()
|
|
||||||
book.parse_chapters()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@@ -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.")
|
@@ -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]
|
@@ -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')
|
317
lector/annotations.py
Normal 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
@@ -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,))
|
||||||
|
|
@@ -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(
|
@@ -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
@@ -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
|
351
lector/guifunctions.py
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
# 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 QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
from lector import database
|
||||||
|
from lector.settings import Settings
|
||||||
|
from lector.resources import resources
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class QImageFactory:
|
||||||
|
def __init__(self, parent):
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
def get_image(self, image_name):
|
||||||
|
icon_theme = self.parent.settings['icon_theme']
|
||||||
|
icon_path = f':/images/{icon_theme}/{image_name}.svg'
|
||||||
|
|
||||||
|
this_qicon = QtGui.QIcon(icon_path)
|
||||||
|
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()
|
@@ -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
|
||||||
|
|
||||||
import database
|
from lector import database
|
||||||
from 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
@@ -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')
|
@@ -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
|
||||||
|
|
||||||
import database
|
from lector import database
|
||||||
|
from lector.widgets import PliantQGraphicsScene
|
||||||
|
from lector.resources import metadata
|
||||||
|
|
||||||
from resources import metadata
|
logger = logging.getLogger(__name__)
|
||||||
from widgets import PliantQGraphicsScene
|
|
||||||
|
|
||||||
|
|
||||||
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()))
|
@@ -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,33 +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 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
|
||||||
|
|
||||||
@@ -65,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', '%', '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
|
||||||
@@ -77,16 +85,23 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
|
|||||||
1: QtCore.Qt.UserRole, # Title
|
1: QtCore.Qt.UserRole, # Title
|
||||||
2: QtCore.Qt.UserRole + 1, # Author
|
2: QtCore.Qt.UserRole + 1, # Author
|
||||||
3: QtCore.Qt.UserRole + 2, # Year
|
3: QtCore.Qt.UserRole + 2, # Year
|
||||||
4: QtCore.Qt.UserRole + 7, # Position percentage
|
4: QtCore.Qt.UserRole + 12, # Last read
|
||||||
5: QtCore.Qt.UserRole + 4} # Tags
|
5: QtCore.Qt.UserRole + 7, # Position percentage
|
||||||
|
6: QtCore.Qt.UserRole + 4} # Tags
|
||||||
self.common_functions = ProxyModelsCommonFunctions(self)
|
self.common_functions = ProxyModelsCommonFunctions(self)
|
||||||
|
|
||||||
def columnCount(self, parent):
|
def columnCount(self, parent):
|
||||||
return 6
|
return 7
|
||||||
|
|
||||||
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
|
||||||
@@ -97,50 +112,41 @@ class TableProxyModel(QtCore.QSortFilterProxyModel):
|
|||||||
source_index = self.mapToSource(index)
|
source_index = self.mapToSource(index)
|
||||||
item = self.sourceModel().item(source_index.row(), 0)
|
item = self.sourceModel().item(source_index.row(), 0)
|
||||||
|
|
||||||
if role == QtCore.Qt.TextAlignmentRole and index.column() == 3:
|
if role == QtCore.Qt.TextAlignmentRole:
|
||||||
return QtCore.Qt.AlignHCenter
|
if index.column() in (3, 4):
|
||||||
|
return QtCore.Qt.AlignHCenter
|
||||||
|
|
||||||
if role == QtCore.Qt.DecorationRole:
|
if role == QtCore.Qt.DecorationRole:
|
||||||
if index.column() == 4:
|
if index.column() == 5:
|
||||||
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, 4): # Cover and Status
|
if index.column() in (0, 5): # Cover and Status
|
||||||
return QtCore.QVariant()
|
return QtCore.QVariant()
|
||||||
|
|
||||||
return item.data(self.role_dictionary[index.column()])
|
if index.column() == 4:
|
||||||
|
last_accessed = item.data(self.role_dictionary[index.column()])
|
||||||
|
if last_accessed:
|
||||||
|
right_now = QtCore.QDateTime().currentDateTime()
|
||||||
|
time_diff = last_accessed.msecsTo(right_now)
|
||||||
|
return self.time_convert(time_diff // 1000)
|
||||||
|
|
||||||
|
return item.data(self.role_dictionary[index.column()])
|
||||||
else:
|
else:
|
||||||
return QtCore.QVariant()
|
return QtCore.QVariant()
|
||||||
|
|
||||||
@@ -152,11 +158,28 @@ 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()
|
||||||
self.sort(0, sorting_order)
|
sorting_order = self.tableViewHeader.sortIndicatorOrder()
|
||||||
self.setSortRole(self.role_dictionary[column])
|
|
||||||
|
|
||||||
|
self.sort(0, sorting_order)
|
||||||
|
if column != 0:
|
||||||
|
self.setSortRole(self.role_dictionary[column])
|
||||||
|
|
||||||
|
def time_convert(self, seconds):
|
||||||
|
seconds = int(seconds)
|
||||||
|
m, s = divmod(seconds, 60)
|
||||||
|
h, m = divmod(m, 60)
|
||||||
|
d, h = divmod(h, 24)
|
||||||
|
|
||||||
|
if d > 0:
|
||||||
|
return f'{d}d'
|
||||||
|
if h > 0:
|
||||||
|
return f'{h}h'
|
||||||
|
if m > 0:
|
||||||
|
return f'{m}m'
|
||||||
|
else:
|
||||||
|
return '<1m'
|
||||||
|
|
||||||
class ProxyModelsCommonFunctions:
|
class ProxyModelsCommonFunctions:
|
||||||
def __init__(self, parent_model):
|
def __init__(self, parent_model):
|
||||||
@@ -175,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
|
||||||
@@ -194,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
|
||||||
@@ -312,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
|
|
80
lector/parsers/comicbooks.py
Normal 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
@@ -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
|
56
lector/parsers/epub.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# 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 zipfile
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from lector.readers.read_epub import EPUB
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ParseEPUB:
|
||||||
|
def __init__(self, filename, temp_dir, file_md5):
|
||||||
|
self.book = None
|
||||||
|
self.filename = filename
|
||||||
|
self.temp_dir = temp_dir
|
||||||
|
self.extract_path = os.path.join(temp_dir, file_md5)
|
||||||
|
|
||||||
|
def read_book(self):
|
||||||
|
self.book = EPUB(self.filename, self.temp_dir)
|
||||||
|
|
||||||
|
def generate_metadata(self):
|
||||||
|
self.book.generate_metadata()
|
||||||
|
return self.book.metadata
|
||||||
|
|
||||||
|
def generate_content(self):
|
||||||
|
zipfile.ZipFile(self.filename).extractall(self.extract_path)
|
||||||
|
|
||||||
|
self.book.generate_toc()
|
||||||
|
self.book.generate_content()
|
||||||
|
|
||||||
|
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
@@ -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
|
54
lector/parsers/markdown.py
Normal 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
|
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
0
lector/resources/__init__.py
Normal 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: <a href="https://github.com/BasioMeusPuga/Lector">https://github.com/BasioMeusPuga/Lector</a></p>
|
<p>Page: <a href="https://github.com/BasioMeusPuga/Lector">https://github.com/BasioMeusPuga/Lector</a></p>
|
||||||
<p>License: GPLv3 <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 <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> </p></body>
|
<p> </p></body>
|
||||||
</html>
|
</html>
|
146
lector/resources/annotationswindow.py
Normal 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"))
|
||||||
|
|
@@ -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, "")
|
@@ -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
|
||||||
|
|
8
lector/resources/raw/DarkIcons/about.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/add.svg
Normal 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 7 3 L 7 7 L 3 7 L 3 9 L 7 9 L 7 13 L 9 13 L 9 9 L 13 9 L 13 7 L 9 7 L 9 3 L 7 3 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 428 B |
8
lector/resources/raw/DarkIcons/annotate.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/arrow-down.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/arrow-up.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/bookmark-new.svg
Normal 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 4 0.00390625 C 4 0.00390625 3 0.00390625 3 1.0039062 L 3 15.003906 L 8 12.003906 L 13 15.003906 L 13 1.0039062 C 13 1.0039062 13 0.00390625 12 0.00390625 L 4 0.00390625 z M 7 3.0039062 L 9 3.0039062 L 9 5.0039062 L 11 5.0039062 L 11 7.0039062 L 9 7.0039062 L 9 9.0039062 L 7 9.0039062 L 7 7.0039062 L 5 7.0039062 L 5 5.0039062 L 7 5.0039062 L 7 3.0039062 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 703 B |
8
lector/resources/raw/DarkIcons/bookmarks.svg
Normal 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 0.390625 L 5.8808594 5.8847656 L 0 6.2011719 L 4.5722656 9.9160156 L 3.0566406 15.607422 L 8 12.40625 L 12.943359 15.607422 L 11.427734 9.9160156 L 16 6.2011719 L 10.119141 5.8847656 L 8 0.390625 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 546 B |
8
lector/resources/raw/DarkIcons/color-picker.svg
Normal 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 C 7.79297 1.66364 7.5132275 2.3110656 7.2109375 2.9472656 C 5.6704375 6.0974656 3.2599437 8.2540875 3.0273438 10.242188 C 3.0213438 10.271888 3.0052 10.304384 3 10.333984 L 3.0195312 10.339844 C 3.0145313 10.408244 3 10.476722 3 10.544922 C 3 13.005122 5.2386 15 8 15 C 10.7614 15 13 13.005122 13 10.544922 C 13 10.476722 12.985469 10.408214 12.980469 10.339844 L 13 10.333984 C 12.995 10.304484 12.978956 10.271887 12.972656 10.242188 C 12.740106 8.2539875 10.329662 6.0973656 8.7890625 2.9472656 C 8.4867825 2.3110456 8.20702 1.6636 8 1 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 891 B |
8
lector/resources/raw/DarkIcons/edit-rename.svg
Normal 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 12.210938 1 C 11.998438 1 11.784141 1.0830469 11.619141 1.2480469 L 9.9902344 2.8886719 L 13.109375 6.0078125 L 14.75 4.3789062 C 15.08 4.0489063 15.08 3.5272656 14.75 3.1972656 L 12.800781 1.2480469 C 12.635781 1.0830469 12.423437 1 12.210938 1 z M 8.8691406 4.0078125 L 0.99023438 11.888672 L 0.99023438 15.007812 L 4.109375 15.007812 L 11.990234 7.1289062 L 8.8691406 4.0078125 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 729 B |
8
lector/resources/raw/DarkIcons/filesaveas.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/format-indent-less.svg
Normal 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 1 1 L 1 3 L 15 3 L 15 1 L 1 1 z M 1 5 L 1 7 L 9 7.0039062 L 9 5.0039062 L 1 5 z M 15 5.0039062 L 10 8.0039062 L 15 11.003906 L 15 5.0039062 z M 1 9 L 1 11 L 9 11 L 9 9 L 1 9 z M 1 13 L 1 15 L 15 15 L 15 13 L 1 13 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 561 B |
8
lector/resources/raw/DarkIcons/format-indent-more.svg
Normal 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 1 1 L 1 3 L 15 3 L 15 1 L 1 1 z M 1 5.0039062 L 1 11.003906 L 6 8.0039062 L 1 5.0039062 z M 7 5.0039062 L 7 7.0039062 L 15 7.0039062 L 15 5.0039062 L 7 5.0039062 z M 15 9 L 7 9.0039062 L 7 11.003906 L 15 11 L 15 9 z M 1 13 L 1 15 L 15 15 L 15 13 L 1 13 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 601 B |
8
lector/resources/raw/DarkIcons/format-justify-center.svg
Normal 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 1 1 L 1 3 L 15 3 L 15 1 L 1 1 z M 4 5.0039062 L 4 7.0039062 L 12 7.0039062 L 12 5.0039062 L 4 5.0039062 z M 4 9.0039062 L 4 11.003906 L 12 11.003906 L 12 9.0039062 L 4 9.0039062 z M 1 13 L 1 15 L 15 15 L 15 13 L 1 13 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 565 B |
8
lector/resources/raw/DarkIcons/format-justify-fill.svg
Normal 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 1 1 L 1 3 L 15 3 L 15 1 L 1 1 z M 1 5.0039062 L 1 7.0039062 L 15 7.0039062 L 15 5.0039062 L 1 5.0039062 z M 1 9.0039062 L 1 11.003906 L 15 11.003906 L 15 9.0039062 L 1 9.0039062 z M 1 13 L 1 15 L 15 15 L 15 13 L 1 13 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 565 B |
8
lector/resources/raw/DarkIcons/format-justify-left.svg
Normal 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 1 1 L 1 3 L 15 3 L 15 1 L 1 1 z M 1 5.0039062 L 1 7.0039062 L 9 7.0039062 L 9 5.0039062 L 1 5.0039062 z M 1 9.0039062 L 1 11.003906 L 9 11.003906 L 9 9.0039062 L 1 9.0039062 z M 1 13 L 1 15 L 15 15 L 15 13 L 1 13 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 561 B |
8
lector/resources/raw/DarkIcons/format-justify-right.svg
Normal 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 1 1 L 1 3 L 15 3 L 15 1 L 1 1 z M 7 5.0039062 L 7 7.0039062 L 15 7.0039062 L 15 5.0039062 L 7 5.0039062 z M 7 9.0039062 L 7 11.003906 L 15 11.003906 L 15 9.0039062 L 7 9.0039062 z M 1 13 L 1 15 L 15 15 L 15 13 L 1 13 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 565 B |
@@ -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 3 L 5 4 L 2 4 L 2 12 L 5 12 L 5 13 L 14 13 L 14 10 L 5 10 L 5 11 L 3 11 L 3 9 L 4 9 L 4 7 L 3 7 L 3 5 L 5 5 L 5 6 L 14 6 L 14 3 L 5 3 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 484 B |
@@ -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 2 L 5 3 L 2 3 L 2 13 L 5 13 L 5 14 L 14 14 L 14 11 L 5 11 L 5 12 L 3 12 L 3 9 L 4 9 L 4 7 L 3 7 L 3 4 L 5 4 L 5 5 L 14 5 L 14 2 L 5 2 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 484 B |
8
lector/resources/raw/DarkIcons/gtk-select-font.svg
Normal 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 7 1 L 2 15 L 4.5 15 L 5.5625 12 L 10.4375 12 L 11.5 15 L 14.28125 15 L 9 1 L 7 1 z M 14 1 A 1 1 0 0 0 13 2 A 1 1 0 0 0 14 3 A 1 1 0 0 0 15 2 A 1 1 0 0 0 14 1 z M 14 4 A 1 1 0 0 0 13 5 A 1 1 0 0 0 14 6 A 1 1 0 0 0 15 5 A 1 1 0 0 0 14 4 z M 8 5 L 9.75 10 L 6.25 10 L 8 5 z M 14 7 A 1 1 0 0 0 13 8 A 1 1 0 0 0 14 9 A 1 1 0 0 0 15 8 A 1 1 0 0 0 14 7 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 694 B |
62
lector/resources/raw/DarkIcons/invert.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/mail-thread-watch.svg
Normal 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="m4.5 3a3.5 5 0 0 0 -3.5 5 3.5 5 0 0 0 3.5 5 3.5 5 0 0 0 3.5 -5 3.5 5 0 0 0 -3.5 -5zm3.5 5a3.5 5 0 0 0 3.5 5 3.5 5 0 0 0 3.5 -5 3.5 5 0 0 0 -3.5 -5 3.5 5 0 0 0 -3.5 5zm-3 0a2 2 0 0 1 2 2 2 2 0 0 1 -2 2 2 2 0 0 1 -2 -2 2 2 0 0 1 2 -2zm7 0a2 2 0 0 1 2 2 2 2 0 0 1 -2 2 2 2 0 0 1 -2 -2 2 2 0 0 1 2 -2z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 642 B |
61
lector/resources/raw/DarkIcons/manga-mode.svg
Normal 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 |
62
lector/resources/raw/DarkIcons/page-double.svg
Normal 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 |
74
lector/resources/raw/DarkIcons/page-flow.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/page-single.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/reload.svg
Normal 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.015625 C 4.134 1.015625 1 4.149625 1 8.015625 C 1 11.881625 4.134 15.015625 8 15.015625 C 11.1748 15.015625 13.86145 12.912425 14.71875 10.015625 L 12.5625 10.015625 C 11.78823 11.775125 10.0457 13.015625 8 13.015625 C 5.2386 13.015625 3 10.777025 3 8.015625 C 3 5.254225 5.2386 3.015625 8 3.015625 C 9.3816 3.015625 10.615525 3.59065 11.515625 4.5 L 9.0058594 7.015625 L 15.005859 7.015625 L 15.005859 1.015625 L 12.953125 3.0683594 C 11.683125 1.8033594 9.9339063 1.015625 8.0039062 1.015625 L 8 1.015625 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 859 B |
8
lector/resources/raw/DarkIcons/remove.svg
Normal 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 3 7 L 3 9 L 13 9 L 13 7 L 3 7 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 378 B |
62
lector/resources/raw/DarkIcons/rotate-left.svg
Normal 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 |
62
lector/resources/raw/DarkIcons/rotate-right.svg
Normal 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 |
53
lector/resources/raw/DarkIcons/search-case.svg
Normal 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 |
68
lector/resources/raw/DarkIcons/search-word.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/search.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/settings.svg
Normal 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 6.25 1 L 6.0957031 2.84375 A 5.5 5.5 0 0 0 4.4882812 3.7734375 L 2.8125 2.984375 L 1.0625 6.015625 L 2.5839844 7.0722656 A 5.5 5.5 0 0 0 2.5 8 A 5.5 5.5 0 0 0 2.5800781 8.9316406 L 1.0625 9.984375 L 2.8125 13.015625 L 4.484375 12.228516 A 5.5 5.5 0 0 0 6.0957031 13.152344 L 6.2460938 15.001953 L 9.7460938 15.001953 L 9.9003906 13.158203 A 5.5 5.5 0 0 0 11.507812 12.228516 L 13.183594 13.017578 L 14.933594 9.9863281 L 13.412109 8.9296875 A 5.5 5.5 0 0 0 13.496094 8.0019531 A 5.5 5.5 0 0 0 13.416016 7.0703125 L 14.933594 6.0175781 L 13.183594 2.9863281 L 11.511719 3.7734375 A 5.5 5.5 0 0 0 9.9003906 2.8496094 L 9.75 1 L 6.25 1 z M 8 6 A 2 2 0 0 1 10 8 A 2 2 0 0 1 8 10 A 2 2 0 0 1 6 8 A 2 2 0 0 1 8 6 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
8
lector/resources/raw/DarkIcons/switches.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/table.svg
Normal 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="m1 1v14h14v-14h-14zm2 2h2v2h-2v-2zm4 0h2v2h-2v-2zm4 0h2v2h-2v-2zm-8 4h2v2h-2v-2zm4 0h2v2h-2v-2zm4 0h2v2h-2v-2zm-8 4h2v2h-2v-2zm4 0h2v2h-2v-2zm4 0h2v2h-2v-2z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 501 B |
8
lector/resources/raw/DarkIcons/tableofcontents.svg
Normal 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 |
8
lector/resources/raw/DarkIcons/trash-empty.svg
Normal 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 6 0.9921875 C 5 0.9921875 5 1.9921875 5 1.9921875 L 2 1.9921875 C 2 1.9921875 1 1.9956938 1 2.9960938 L 1 3.9960938 L 14 3.9921875 L 14 2.9960938 C 14 1.9960938 13 1.9921875 13 1.9921875 L 10 1.9921875 C 10 1.9921875 10 0.9921875 9 0.9921875 L 6 0.9921875 z M 2 4.9960938 L 2 13.996094 C 2.00005 14.519674 2.47642 14.996044 3 14.996094 L 12 14.996094 C 12.52358 14.996044 12.99995 14.519674 13 13.996094 L 13 4.9960938 L 2 4.9960938 z" transform="translate(4 4)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 781 B |