Codebase list sugar-read-activity / df748c6
New upstream version 123 Jonas Smedegaard 3 years ago
44 changed file(s) with 2482 addition(s) and 3787 deletion(s). Raw diff Collapse all Expand all
0 123
1
2 * Use named arguments in translatable strings (Hrishi Patel),
3 * Add dark mode for PDFs (Swarup N),
4 * Port to Python 3 (Rahul Bothra, James Cameron),
5 * Text objects show left justify and monospaced (James Cameron),
6 * Remove speech language choice (James Cameron),
7
8 122
9
10 * Restore EPUB support for WebKit 3.0 API (Lubomir Rintel),
11
012 121
113
214 * Add README.md (Rudra Sadhu),
11 name = Read
22 bundle_id = org.laptop.sugar.ReadActivity
33 icon = activity-read
4 exec = sugar-activity readactivity.ReadActivity
5 activity_version = 121
6 mime_types = application/pdf;image/vnd.djvu;image/x.djvu;image/tiff;text/plain;application/zip;application/x-cbz
4 exec = sugar-activity3 readactivity.ReadActivity
5 activity_version = 123
6 mime_types = application/pdf;image/vnd.djvu;image/x.djvu;image/tiff;application/epub+zip;text/plain;application/zip;application/x-cbz
77 max_participants = 10
88 license = GPLv2+;LGPLv2+
99 summary = Use this activity when you are ready to read! Remember to flip your computer around to feel like you are really holding a book!
10 tags = Language;Documents;Media;System
10 tags = Language;Documents;Media
1111 url = https://help.sugarlabs.org/en/read.html
1212 repository = https://github.com/sugarlabs/read-activity
0 <?xml version="1.0" encoding="UTF-8"?>
1 <mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
2 <mime-type type="application/epub+zip">
3 <comment xml:lang="en">Epub document</comment>
4 <glob pattern="*.epub"/>
5 </mime-type>
6 </mime-info>
104104 # TRANS: (the elapsed string gets translated automatically)
105105 tooltip_footer = (
106106 _('Bookmark added by %(user)s %(time)s')
107 % {'user': bookmark.nick.decode('utf-8'),
108 'time': time.decode('utf-8')})
107 % {'user': bookmark.nick,
108 'time': time})
109109
110110 l = Gtk.Label('<big>%s</big>' % tooltip_header)
111111 l.set_use_markup(True)
195195 dialog.show_all()
196196
197197 def _real_add_bookmark(self, page, content):
198 self._bookmark_manager.add_bookmark(page, unicode(content))
198 self._bookmark_manager.add_bookmark(page, str(content))
199199 self.update_for_page(page)
200200
201201 def del_bookmark(self, page):
0 from gi.repository import GObject
1 import logging
2
3 import epubview
4
5 # import speech
6
7 from io import StringIO
8
9 _logger = logging.getLogger('read-activity')
10
11
12 class EpubViewer(epubview.EpubView):
13
14 def __init__(self):
15 epubview.EpubView.__init__(self)
16
17 def setup(self, activity):
18 self.set_screen_dpi(activity.dpi)
19 self.connect('selection-changed',
20 activity._view_selection_changed_cb)
21
22 activity._hbox.pack_start(self, True, True, 0)
23 self.show_all()
24 self._modified_files = []
25
26 # text to speech initialization
27 self.current_word = 0
28 self.word_tuples = []
29
30 def load_document(self, file_path):
31 self.set_document(EpubDocument(self, file_path.replace('file://', '')))
32 # speech.highlight_cb = self.highlight_next_word
33 # speech.reset_cb = self.reset_text_to_speech
34 # speech.end_text_cb = self.get_more_text
35
36 def load_metadata(self, activity):
37
38 self.metadata = activity.metadata
39
40 if not self.metadata['title_set_by_user'] == '1':
41 title = self._epub._info._get_title()
42 if title:
43 self.metadata['title'] = title
44 if 'Read_zoom' in self.metadata:
45 try:
46 logging.error('Loading zoom %s', self.metadata['Read_zoom'])
47 self.set_zoom(float(self.metadata['Read_zoom']))
48 except:
49 pass
50
51 def update_metadata(self, activity):
52 self.metadata = activity.metadata
53 self.metadata['Read_zoom'] = str(self.get_zoom())
54
55 def zoom_to_width(self):
56 pass
57
58 def zoom_to_best_fit(self):
59 pass
60
61 def zoom_to_actual_size(self):
62 pass
63
64 def can_zoom_to_width(self):
65 return False
66
67 def can_highlight(self):
68 return True
69
70 def show_highlights(self, page):
71 # we save the highlights in the page as html
72 pass
73
74 def toggle_highlight(self, highlight):
75 self._view.set_editable(True)
76
77 if highlight:
78 js = 'document.execCommand("backColor", false, "yellow");'
79 else:
80 # need remove the highlight nodes
81 js = '''
82 (function(){
83 var selObj = window.getSelection();
84 if (selObj.rangeCount < 1)
85 return;
86 var range = selObj.getRangeAt(0);
87 var node = range.startContainer;
88 while (node.parentNode != null) {
89 if (node.localName == "span") {
90 if (node.hasAttributes()) {
91 var attrs = node.attributes;
92 for (var i = attrs.length - 1; i >= 0; i--) {
93 if (attrs[i].name == "style" &&
94 attrs[i].value == "background-color: yellow;") {
95 node.removeAttribute("style");
96 break;
97 };
98 };
99 };
100 };
101 node = node.parentNode;
102 };
103 }())
104 '''
105
106 self._view.run_javascript(js)
107
108 self._view.set_editable(False)
109 # mark the file as modified
110 current_file = self.get_current_file()
111 logging.error('file %s was modified', current_file)
112 if current_file not in self._modified_files:
113 self._modified_files.append(current_file)
114 GObject.idle_add(self._save_page)
115
116 def _save_page(self):
117 html = self._view._execute_script_sync("document.documentElement.innerHTML")
118 file_path = self.get_current_file().replace('file:///', '/')
119 logging.error(html)
120 with open(file_path, 'w') as fd:
121 header = """<?xml version="1.0" encoding="utf-8" standalone="no"?>
122 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
123 "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
124 <html xmlns="http://www.w3.org/1999/xhtml">"""
125 fd.write(header)
126 fd.write(html)
127 fd.write('</html>')
128
129 def save(self, file_path):
130 if self._modified_files:
131 self._epub.write(file_path)
132 return True
133
134 return False
135
136 def in_highlight(self):
137 # Verify if the selection already exist or the cursor
138 # is in a highlighted area
139 return self._view._execute_script_sync("""
140 (function(){
141 var selObj = window.getSelection();
142 if (selObj.rangeCount < 1)
143 return false;
144 var range = selObj.getRangeAt(0);
145 var node = range.startContainer;
146 while (node.parentNode != null) {
147 if (node.localName == "span") {
148 if (node.hasAttributes()) {
149 var attrs = node.attributes;
150 for(var i = attrs.length - 1; i >= 0; i--) {
151 if (attrs[i].name == "style" &&
152 attrs[i].value == "background-color: yellow;") {
153 return true;
154 };
155 };
156 };
157 };
158 node = node.parentNode;
159 };
160 return false;
161 })()""") == "true", None;
162
163 def can_do_text_to_speech(self):
164 return False
165
166 def can_rotate(self):
167 return False
168
169 def get_marked_words(self):
170 "Adds a mark between each word of text."
171 i = self.current_word
172 file_str = StringIO()
173 file_str.write('<speak> ')
174 end_range = i + 40
175 if end_range > len(self.word_tuples):
176 end_range = len(self.word_tuples)
177 for word_tuple in self.word_tuples[self.current_word:end_range]:
178 file_str.write('<mark name="' + str(i) + '"/>' +
179 word_tuple[2].encode('utf-8'))
180 i = i + 1
181 self.current_word = i
182 file_str.write('</speak>')
183 return file_str.getvalue()
184
185 def get_more_text(self):
186 pass
187 """
188 if self.current_word < len(self.word_tuples):
189 speech.stop()
190 more_text = self.get_marked_words()
191 speech.play(more_text)
192 else:
193 if speech.reset_buttons_cb is not None:
194 speech.reset_buttons_cb()
195 """
196
197 def reset_text_to_speech(self):
198 self.current_word = 0
199
200 def highlight_next_word(self, word_count):
201 pass
202 """
203 TODO: disabled because javascript can't be executed
204 with the velocity needed
205 self.current_word = word_count
206 self._view.highlight_next_word()
207 return True
208 """
209
210 def connect_zoom_handler(self, handler):
211 self._zoom_handler = handler
212 self._view_notify_zoom_handler = \
213 self.connect('notify::scale', handler)
214 return self._view_notify_zoom_handler
215
216 def connect_page_changed_handler(self, handler):
217 self.connect('page-changed', handler)
218
219 def _try_load_page(self, n):
220 if self._ready:
221 self._load_page(n)
222 return False
223 else:
224 return True
225
226 def set_screen_dpi(self, dpi):
227 return
228
229 def find_set_highlight_search(self, set_highlight_search):
230 pass
231
232 def set_current_page(self, n):
233 # When the book is being loaded, calling this does not help
234 # In such a situation, we go into a loop and try to load the
235 # supplied page when the book has loaded completely
236 n += 1
237 if self._ready:
238 self._load_page(n)
239 else:
240 GObject.timeout_add(200, self._try_load_page, n)
241
242 def get_current_page(self):
243 return int(self._loaded_page) - 1
244
245 def get_current_link(self):
246 # the _loaded_filename include all the path,
247 # need only the part included in the link
248 return self._loaded_filename[len(self._epub._tempdir) + 1:]
249
250 def update_toc(self, activity):
251 if self._epub.has_document_links():
252 activity.show_navigator_button()
253 activity.set_navigator_model(self._epub.get_links_model())
254 return True
255 else:
256 return False
257
258 def get_link_iter(self, current_link):
259 """
260 Returns the iter related to a link
261 """
262 link_iter = self._epub.get_links_model().get_iter_first()
263
264 while link_iter is not None and \
265 self._epub.get_links_model().get_value(link_iter, 1) \
266 != current_link:
267 link_iter = self._epub.get_links_model().iter_next(link_iter)
268 return link_iter
269
270 def find_changed(self, job, page=None):
271 self._find_changed(job)
272
273 def handle_link(self, link):
274 self._load_file(link)
275
276 def setup_find_job(self, text, updated_cb):
277 self._find_job = JobFind(document=self._epub,
278 start_page=0, n_pages=self.get_pagecount(),
279 text=text, case_sensitive=False)
280 self._find_updated_handler = self._find_job.connect('updated',
281 updated_cb)
282 return self._find_job, self._find_updated_handler
283
284
285 class EpubDocument(epubview.Epub):
286
287 def __init__(self, view, docpath):
288 epubview.Epub.__init__(self, docpath)
289 self._page_cache = view
290
291 def get_n_pages(self):
292 return int(self._page_cache.get_pagecount())
293
294 def has_document_links(self):
295 return True
296
297 def get_links_model(self):
298 return self.get_toc_model()
299
300
301 class JobFind(epubview.JobFind):
302
303 def __init__(self, document, start_page, n_pages, text,
304 case_sensitive=False):
305 epubview.JobFind.__init__(self, document, start_page, n_pages, text,
306 case_sensitive=False)
0 # Copyright 2009 One Laptop Per Child
1 # Author: Sayamindu Dasgupta <sayamindu@laptop.org>
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16
17 from .epub import _Epub as Epub
18 from .epubview import _View as EpubView
19 from .jobs import _JobFind as JobFind
0 # Copyright 2009 One Laptop Per Child
1 # Author: Sayamindu Dasgupta <sayamindu@laptop.org>
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16
17 import zipfile
18 import tempfile
19 import os
20 import xml.etree.ElementTree as etree
21 import shutil
22 import logging
23
24 from . import navmap
25 from . import epubinfo
26
27
28 class _Epub(object):
29
30 def __init__(self, _file):
31 """
32 _file: can be either a path to a file (a string) or a file-like object.
33 """
34 self._file = _file
35 self._zobject = None
36 self._opfpath = None
37 self._ncxpath = None
38 self._basepath = None
39 self._tempdir = tempfile.mkdtemp()
40
41 if not self._verify():
42 print('Warning: This does not seem to be a valid epub file')
43
44 self._get_opf()
45 self._get_ncx()
46
47 ncxfile = self._zobject.open(self._ncxpath)
48 opffile = self._zobject.open(self._opfpath)
49 self._navmap = navmap.NavMap(opffile, ncxfile, self._basepath)
50
51 opffile = self._zobject.open(self._opfpath)
52 self._info = epubinfo.EpubInfo(opffile)
53
54 self._unzip()
55
56 def _unzip(self):
57 # This is broken upto python 2.7
58 # self._zobject.extractall(path = self._tempdir)
59 orig_cwd = os.getcwd()
60 os.chdir(self._tempdir)
61 for name in self._zobject.namelist():
62 # Some weird zip file entries start with a slash,
63 # and we don't want to write to the root directory
64 try:
65 if name.startswith(os.path.sep):
66 name = name[1:]
67 if name.endswith(os.path.sep) or name.endswith('\\'):
68 os.makedirs(name)
69 except:
70 logging.error('ERROR unziping %s', name)
71 else:
72 self._zobject.extract(name)
73 os.chdir(orig_cwd)
74
75 def _get_opf(self):
76 containerfile = self._zobject.open('META-INF/container.xml')
77
78 tree = etree.parse(containerfile)
79 root = tree.getroot()
80
81 r_id = './/{urn:oasis:names:tc:opendocument:xmlns:container}rootfile'
82 for element in root.iterfind(r_id):
83 if element.get('media-type') == 'application/oebps-package+xml':
84 self._opfpath = element.get('full-path')
85
86 if self._opfpath.rpartition('/')[0]:
87 self._basepath = self._opfpath.rpartition('/')[0] + '/'
88 else:
89 self._basepath = ''
90
91 containerfile.close()
92
93 def _get_ncx(self):
94 opffile = self._zobject.open(self._opfpath)
95
96 tree = etree.parse(opffile)
97 root = tree.getroot()
98
99 spine = root.find('.//{http://www.idpf.org/2007/opf}spine')
100 tocid = spine.get('toc')
101
102 for element in root.iterfind('.//{http://www.idpf.org/2007/opf}item'):
103 if element.get('id') == tocid:
104 self._ncxpath = self._basepath + element.get('href')
105
106 opffile.close()
107
108 def _verify(self):
109 '''
110 Method to crudely check to verify that what we
111 are dealing with is a epub file or not
112 '''
113 if isinstance(self._file, str):
114 if not os.path.exists(self._file):
115 return False
116
117 self._zobject = zipfile.ZipFile(self._file)
118
119 if 'mimetype' not in self._zobject.namelist():
120 return False
121
122 mtypefile = self._zobject.open('mimetype')
123 mimetype = mtypefile.readline()
124
125 # Some files seem to have trailing characters
126 if not mimetype.startswith(b'application/epub+zip'):
127 return False
128
129 return True
130
131 def get_toc_model(self):
132 '''
133 Returns a GtkTreeModel representation of the
134 Epub table of contents
135 '''
136 return self._navmap.get_gtktreestore()
137
138 def get_flattoc(self):
139 '''
140 Returns a flat (linear) list of files to be
141 rendered.
142 '''
143 return self._navmap.get_flattoc()
144
145 def get_basedir(self):
146 '''
147 Returns the base directory where the contents of the
148 epub has been unzipped
149 '''
150 return self._tempdir
151
152 def get_info(self):
153 '''
154 Returns a EpubInfo object title
155 '''
156 return self._info.title
157
158 def write(self, file_path):
159 '''Create the ZIP archive.
160 The mimetype must be the first file in the archive
161 and it must not be compressed.'''
162
163 # The EPUB must contain the META-INF and mimetype files at the root, so
164 # we'll create the archive in the working directory first
165 # and move it later
166 current_dir = os.getcwd()
167 os.chdir(self._tempdir)
168
169 # Open a new zipfile for writing
170 epub = zipfile.ZipFile(file_path, 'w')
171
172 # Add the mimetype file first and set it to be uncompressed
173 epub.write('mimetype', compress_type=zipfile.ZIP_STORED)
174
175 # For the remaining paths in the EPUB, add all of their files
176 # using normal ZIP compression
177 self._scan_dir('.', epub)
178
179 epub.close()
180 os.chdir(current_dir)
181
182 def _scan_dir(self, path, epub_file):
183 for p in os.listdir(path):
184 logging.error('add file %s', p)
185 if os.path.isdir(os.path.join(path, p)):
186 self._scan_dir(os.path.join(path, p), epub_file)
187 else:
188 if p != 'mimetype':
189 epub_file.write(
190 os.path.join(path, p),
191 compress_type=zipfile.ZIP_DEFLATED)
192
193 def close(self):
194 '''
195 Cleans up (closes open zip files and deletes
196 uncompressed content of Epub.
197 Please call this when a file is being closed or during
198 application exit.
199 '''
200 self._zobject.close()
201 shutil.rmtree(self._tempdir)
0 import xml.etree.ElementTree as etree
1
2
3 class EpubInfo():
4
5 # TODO: Cover the entire DC range
6
7 def __init__(self, opffile):
8 self._tree = etree.parse(opffile)
9 self._root = self._tree.getroot()
10 self._e_metadata = self._root.find(
11 '{http://www.idpf.org/2007/opf}metadata')
12
13 self.title = self._get_title()
14 self.creator = self._get_creator()
15 self.date = self._get_date()
16 self.subject = self._get_subject()
17 self.source = self._get_source()
18 self.rights = self._get_rights()
19 self.identifier = self._get_identifier()
20 self.language = self._get_language()
21 self.summary = self._get_description()
22 self.cover_image = self._get_cover_image()
23
24 def _get_data(self, tagname):
25 element = self._e_metadata.find(tagname)
26 return element.text
27
28 def _get_title(self):
29 try:
30 ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}title')
31 except AttributeError:
32 return None
33
34 return ret
35
36 def _get_description(self):
37 try:
38 ret = self._get_data(
39 './/{http://purl.org/dc/elements/1.1/}description')
40 except AttributeError:
41 return None
42
43 return ret
44
45 def _get_creator(self):
46 try:
47 ret = self._get_data(
48 './/{http://purl.org/dc/elements/1.1/}creator')
49 except AttributeError:
50 return None
51 return ret
52
53 def _get_date(self):
54 # TODO: iter
55 try:
56 ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}date')
57 except AttributeError:
58 return None
59
60 return ret
61
62 def _get_source(self):
63 try:
64 ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}source')
65 except AttributeError:
66 return None
67
68 return ret
69
70 def _get_rights(self):
71 try:
72 ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}rights')
73 except AttributeError:
74 return None
75
76 return ret
77
78 def _get_identifier(self):
79 # TODO: iter
80 element = self._e_metadata.find(
81 './/{http://purl.org/dc/elements/1.1/}identifier')
82
83 if element is not None:
84 return {'id': element.get('id'), 'value': element.text}
85 else:
86 return None
87
88 def _get_language(self):
89 try:
90 ret = self._get_data(
91 './/{http://purl.org/dc/elements/1.1/}language')
92 except AttributeError:
93 return None
94
95 return ret
96
97 def _get_subject(self):
98 try:
99 subjectlist = []
100 for element in self._e_metadata.iterfind(
101 './/{http://purl.org/dc/elements/1.1/}subject'):
102 subjectlist.append(element.text)
103 except AttributeError:
104 return None
105
106 return subjectlist
107
108 def _get_cover_image(self):
109 element = self._e_metadata.find('{http://www.idpf.org/2007/opf}meta')
110 if element is not None and element.get('name') == 'cover':
111 return element.get('content')
112 else:
113 return None
0 # Copyright 2009 One Laptop Per Child
1 # Author: Sayamindu Dasgupta <sayamindu@laptop.org>
2 # WebKit2 port Copyright (C) 2018 Lubomir Rintel <lkundrak@v3.sk>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import gi
19 gi.require_version('WebKit2', '4.0')
20
21 from gi.repository import Gtk
22 from gi.repository import GObject
23 from gi.repository import Gdk
24 from gi.repository import WebKit2
25 from . import widgets
26
27 import logging
28 import os.path
29 import math
30 import shutil
31
32 from .jobs import _JobPaginator as _Paginator
33
34 LOADING_HTML = '''
35 <html style="height: 100%; margin: 0; padding: 0; width: 100%;">
36 <body style="display: table; height: 100%; margin: 0; padding: 0; width: 100%;">
37 <div style="display: table-cell; text-align: center; vertical-align: middle;">
38 <h1>Loading...</h1>
39 </div>
40 </body>
41 </html>
42 '''
43
44 class _View(Gtk.HBox):
45
46 __gproperties__ = {
47 'scale': (GObject.TYPE_FLOAT, 'the zoom level',
48 'the zoom level of the widget',
49 0.5, 4.0, 1.0, GObject.PARAM_READWRITE),
50 }
51 __gsignals__ = {
52 'page-changed': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
53 ([int, int])),
54 'selection-changed': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
55 ([])),
56 }
57
58 def __init__(self):
59 GObject.threads_init()
60 Gtk.HBox.__init__(self)
61
62 self.connect("destroy", self._destroy_cb)
63
64 self._ready = False
65 self._paginator = None
66 self._loaded_page = -1
67 # self._old_scrollval = -1
68 self._loaded_filename = None
69 self._pagecount = -1
70 self.__scroll_to_end = False
71 self.__page_changed = False
72 self._has_selection = False
73 self._scrollval = 0.0
74 self.scale = 1.0
75 self._epub = None
76 self._findjob = None
77 self.__in_search = False
78 self.__search_fwd = True
79 self._filelist = None
80 self._internal_link = None
81
82 self._view = widgets._WebView()
83 self._view.load_html(LOADING_HTML, '/')
84 settings = self._view.get_settings()
85 settings.props.default_font_family = 'DejaVu LGC Serif'
86 settings.props.enable_plugins = False
87 settings.props.default_charset = 'utf-8'
88 self._view.connect('load-changed', self._view_load_changed_cb)
89 self._view.connect('scrolled', self._view_scrolled_cb)
90 self._view.connect('scrolled-top', self._view_scrolled_top_cb)
91 self._view.connect('scrolled-bottom', self._view_scrolled_bottom_cb)
92 self._view.connect('selection-changed', self._view_selection_changed_cb)
93
94 find = self._view.get_find_controller()
95 find.connect('failed-to-find-text', self._find_failed_cb)
96
97 self._eventbox = Gtk.EventBox()
98 self._eventbox.connect('scroll-event', self._eventbox_scroll_event_cb)
99 self._eventbox.add_events(Gdk.EventMask.SCROLL_MASK)
100 self._eventbox.add(self._view)
101
102 self._scrollbar = Gtk.VScrollbar()
103 self._scrollbar_change_value_cb_id = self._scrollbar.connect(
104 'change-value', self._scrollbar_change_value_cb)
105
106 hbox = Gtk.HBox()
107 hbox.pack_start(self._eventbox, True, True, 0)
108 hbox.pack_end(self._scrollbar, False, True, 0)
109
110 self.pack_start(hbox, True, True, 0)
111 self._view.set_can_default(True)
112 self._view.set_can_focus(True)
113
114 def map_cp(widget):
115 widget.setup_touch()
116 widget.disconnect(self._setup_handle)
117
118 self._setup_handle = self._view.connect('map', map_cp)
119
120 def set_document(self, epubdocumentinstance):
121 '''
122 Sets document (should be a Epub instance)
123 '''
124 self._epub = epubdocumentinstance
125 GObject.idle_add(self._paginate)
126
127 def do_get_property(self, property):
128 if property.name == 'has-selection':
129 return self._has_selection
130 elif property.name == 'scale':
131 return self.scale
132 else:
133 raise AttributeError('unknown property %s' % property.name)
134
135 def do_set_property(self, property, value):
136 if property.name == 'scale':
137 self.__set_zoom(value)
138 else:
139 raise AttributeError('unknown property %s' % property.name)
140
141 def get_has_selection(self):
142 '''
143 Returns True if any part of the content is selected
144 '''
145 return self._has_selection
146
147 def get_zoom(self):
148 '''
149 Returns the current zoom level
150 '''
151 return self.get_property('scale') * 100.0
152
153 def set_zoom(self, value):
154 '''
155 Sets the current zoom level
156 '''
157 scrollbar_pos = self.get_vertical_pos()
158 self._view.set_zoom_level(value / 100.0)
159 self.set_vertical_pos(scrollbar_pos)
160
161 def _get_scale(self):
162 '''
163 Returns the current zoom level
164 '''
165 return self.get_property('scale')
166
167 def _set_scale(self, value):
168 '''
169 Sets the current zoom level
170 '''
171 self.set_property('scale', value)
172
173 def zoom_in(self):
174 '''
175 Zooms in (increases zoom level by 0.1)
176 '''
177 if self.can_zoom_in():
178 scrollbar_pos = self.get_vertical_pos()
179 self._set_scale(self._get_scale() + 0.1)
180 self.set_vertical_pos(scrollbar_pos)
181 return True
182 else:
183 return False
184
185 def zoom_out(self):
186 '''
187 Zooms out (decreases zoom level by 0.1)
188 '''
189 if self.can_zoom_out():
190 scrollbar_pos = self.get_vertical_pos()
191 self._set_scale(self._get_scale() - 0.1)
192 self.set_vertical_pos(scrollbar_pos)
193 return True
194 else:
195 return False
196
197 def get_vertical_pos(self):
198 """
199 Used to save the scrolled position and restore when needed
200 """
201 return self._scrollval
202
203 def set_vertical_pos(self, position):
204 """
205 Used to save the scrolled position and restore when needed
206 """
207 self._view.scroll_to(position)
208
209 def can_zoom_in(self):
210 '''
211 Returns True if it is possible to zoom in further
212 '''
213 if self.scale < 4:
214 return True
215 else:
216 return False
217
218 def can_zoom_out(self):
219 '''
220 Returns True if it is possible to zoom out further
221 '''
222 if self.scale > 0.5:
223 return True
224 else:
225 return False
226
227 def get_current_page(self):
228 '''
229 Returns the currently loaded page
230 '''
231 return self._loaded_page
232
233 def get_current_file(self):
234 '''
235 Returns the currently loaded XML file
236 '''
237 # return self._loaded_filename
238 if self._paginator:
239 return self._paginator.get_file_for_pageno(self._loaded_page)
240 else:
241 return None
242
243 def get_pagecount(self):
244 '''
245 Returns the pagecount of the loaded file
246 '''
247 return self._pagecount
248
249 def set_current_page(self, n):
250 '''
251 Loads page number n
252 '''
253 if n < 1 or n > self._pagecount:
254 return False
255 self._load_page(n)
256 return True
257
258 def next_page(self):
259 '''
260 Loads next page if possible
261 Returns True if transition to next page is possible and done
262 '''
263 if self._loaded_page == self._pagecount:
264 return False
265 self._load_next_page()
266 return True
267
268 def previous_page(self):
269 '''
270 Loads previous page if possible
271 Returns True if transition to previous page is possible and done
272 '''
273 if self._loaded_page == 1:
274 return False
275 self._load_prev_page()
276 return True
277
278 def scroll(self, scrolltype, horizontal):
279 '''
280 Scrolls through the pages.
281 Scrolling is horizontal if horizontal is set to True
282 Valid scrolltypes are:
283 Gtk.ScrollType.PAGE_BACKWARD, Gtk.ScrollType.PAGE_FORWARD,
284 Gtk.ScrollType.STEP_BACKWARD, Gtk.ScrollType.STEP_FORWARD
285 Gtk.ScrollType.STEP_START and Gtk.ScrollType.STEP_END
286 '''
287 if scrolltype == Gtk.ScrollType.PAGE_BACKWARD:
288 pages = self._paginator.get_pagecount_for_file(self._loaded_filename)
289 self._view.scroll_by(self._page_height / pages * -1)
290 elif scrolltype == Gtk.ScrollType.PAGE_FORWARD:
291 pages = self._paginator.get_pagecount_for_file(self._loaded_filename)
292 self._view.scroll_by(self._page_height / pages * 1)
293 elif scrolltype == Gtk.ScrollType.STEP_BACKWARD:
294 self._view.scroll_by(self._view.get_settings().get_default_font_size() * -3)
295 elif scrolltype == Gtk.ScrollType.STEP_FORWARD:
296 self._view.scroll_by(self._view.get_settings().get_default_font_size() * 3)
297 elif scrolltype == Gtk.ScrollType.START:
298 self.set_current_page(0)
299 elif scrolltype == Gtk.ScrollType.END:
300 self.__scroll_to_end = True
301 self.set_current_page(self._pagecount - 1)
302 else:
303 print('Got unsupported scrolltype %s' % str(scrolltype))
304
305 def __touch_page_changed_cb(self, widget, forward):
306 if forward:
307 self.scroll(Gtk.ScrollType.PAGE_FORWARD, False)
308 else:
309 self.scroll(Gtk.ScrollType.PAGE_BACKWARD, False)
310
311 def copy(self):
312 '''
313 Copies the current selection to clipboard.
314 '''
315 self._view.run_javascript('document.execCommand("copy")')
316
317 def find_next(self):
318 '''
319 Highlights the next matching item for current search
320 '''
321 self._view.grab_focus()
322 self.__search_fwd = True
323 self._view.get_find_controller().search_next()
324
325 def find_previous(self):
326 '''
327 Highlights the previous matching item for current search
328 '''
329 self._view.grab_focus()
330 self.__search_fwd = False
331 self._view.get_find_controller().search_previous()
332
333 def _find_failed_cb(self, find_controller):
334 try:
335 if self.__search_fwd:
336 path = os.path.join(self._epub.get_basedir(),
337 self._findjob.get_next_file())
338 else:
339 path = os.path.join(self._epub.get_basedir(),
340 self._findjob.get_prev_file())
341 self.__in_search = True
342 self._load_file(path)
343 except IndexError:
344 # No match anywhere, no other file to pick
345 pass
346
347 def _find_changed(self, job):
348 self._view.grab_focus()
349 self._findjob = job
350 find = self._view.get_find_controller()
351 find.search (self._findjob.get_search_text(),
352 self._findjob.get_flags(),
353 GObject.G_MAXUINT)
354
355 def __set_zoom(self, value):
356 self._view.set_zoom_level(value)
357 self.scale = value
358
359 def _view_scrolled_cb(self, view, scrollval):
360 if self._loaded_page < 1:
361 return
362
363 self._scrollval = scrollval
364 scroll_upper = self._page_height
365 scroll_page_size = self._view.get_allocated_height()
366
367 if scrollval > 0:
368 scrollfactor = scrollval / (scroll_upper - scroll_page_size)
369 else:
370 scrollfactor = 0
371
372 if not self._loaded_page == self._pagecount and \
373 not self._paginator.get_file_for_pageno(self._loaded_page) != \
374 self._paginator.get_file_for_pageno(self._loaded_page + 1):
375
376 scrollfactor_next = \
377 self._paginator.get_scrollfactor_pos_for_pageno(
378 self._loaded_page + 1)
379 if scrollfactor >= scrollfactor_next:
380 self._on_page_changed(self._loaded_page, self._loaded_page + 1)
381 return
382
383 if self._loaded_page > 1 and \
384 not self._paginator.get_file_for_pageno(self._loaded_page) != \
385 self._paginator.get_file_for_pageno(self._loaded_page - 1):
386
387 scrollfactor_cur = \
388 self._paginator.get_scrollfactor_pos_for_pageno(
389 self._loaded_page)
390 if scrollfactor <= scrollfactor_cur:
391 self._on_page_changed(self._loaded_page, self._loaded_page - 1)
392 return
393
394 def _view_scrolled_top_cb(self, view):
395 if self._loaded_page > 1:
396 self.__scroll_to_end = True
397 self._load_prev_page()
398
399 def _view_scrolled_bottom_cb(self, view):
400 if self._loaded_page < self._pagecount:
401 self._load_next_page()
402
403 def _view_selection_changed_cb(self, view, has_selection):
404 self._has_selection = has_selection
405 self.emit('selection-changed')
406
407 def _eventbox_scroll_event_cb(self, view, event):
408 if event.direction == Gdk.ScrollDirection.DOWN:
409 self.scroll(Gtk.ScrollType.STEP_FORWARD, False)
410 elif event.direction == Gdk.ScrollDirection.UP:
411 self.scroll(Gtk.ScrollType.STEP_BACKWARD, False)
412
413 def _view_load_changed_cb(self, v, load_event):
414 if load_event != WebKit2.LoadEvent.FINISHED:
415 return True
416
417 filename = self._view.props.uri.replace('file://', '')
418 if os.path.exists(filename.replace('xhtml', 'xml')):
419 # Hack for making javascript work
420 filename = filename.replace('xhtml', 'xml')
421
422 filename = filename.split('#')[0] # Get rid of anchors
423
424 if self._loaded_page < 1 or filename is None:
425 return False
426
427 self._loaded_filename = filename
428
429 remfactor = self._paginator.get_remfactor_for_file(filename)
430 pages = self._paginator.get_pagecount_for_file(filename)
431 extra = int(math.ceil(
432 remfactor * self._view.get_page_height() / (pages - remfactor)))
433 if extra > 0:
434 self._view.add_bottom_padding(extra)
435 self._page_height = self._view.get_page_height()
436
437 if self.__in_search:
438 self.__in_search = False
439 find = self._view.get_find_controller()
440 find.search (self._findjob.get_search_text(),
441 self._findjob.get_flags(self.__search_fwd),
442 GObject.G_MAXUINT)
443 else:
444 self._scroll_page()
445
446 # process_file = True
447 if self._internal_link is not None:
448 self._view.go_to_link(self._internal_link)
449 vertical_pos = \
450 self._view.get_vertical_position_element(self._internal_link)
451 # set the page number based in the vertical position
452 initial_page = self._paginator.get_base_pageno_for_file(filename)
453 self._loaded_page = initial_page + int(
454 vertical_pos / self._paginator.get_single_page_height())
455
456 # There are epub files, created with Calibre,
457 # where the link in the index points to the end of the previos
458 # file to the needed chapter.
459 # if the link is at the bottom of the page, we open the next file
460 one_page_height = self._paginator.get_single_page_height()
461 self._internal_link = None
462 if vertical_pos > self._page_height - one_page_height:
463 logging.error('bottom page link, go to next file')
464 next_file = self._paginator.get_next_filename(filename)
465 if next_file is not None:
466 logging.error('load next file %s', next_file)
467 self.__in_search = False
468 self.__scroll_to_end = False
469 # process_file = False
470 GObject.idle_add(self._load_file, next_file)
471
472 # if process_file:
473 # # prepare text to speech
474 # html_file = open(self._loaded_filename)
475 # soup = BeautifulSoup.BeautifulSoup(html_file)
476 # body = soup.find('body')
477 # tags = body.findAll(text=True)
478 # self._all_text = ''.join([tag for tag in tags])
479 # self._prepare_text_to_speech(self._all_text)
480
481 def _prepare_text_to_speech(self, page_text):
482 i = 0
483 j = 0
484 word_begin = 0
485 word_end = 0
486 ignore_chars = [' ', '\n', '\r', '_', '[', '{', ']', '}', '|',
487 '<', '>', '*', '+', '/', '\\']
488 ignore_set = set(ignore_chars)
489 self.word_tuples = []
490 len_page_text = len(page_text)
491 while i < len_page_text:
492 if page_text[i] not in ignore_set:
493 word_begin = i
494 j = i
495 while j < len_page_text and page_text[j] not in ignore_set:
496 j = j + 1
497 word_end = j
498 i = j
499 word_tuple = (word_begin, word_end,
500 page_text[word_begin: word_end])
501 if word_tuple[2] != '\r':
502 self.word_tuples.append(word_tuple)
503 i = i + 1
504
505 def _scroll_page(self):
506 v_upper = self._page_height
507 if self.__scroll_to_end:
508 # We need to scroll to the last page
509 scrollval = v_upper
510 self.__scroll_to_end = False
511 else:
512 pageno = self._loaded_page
513 scrollfactor = self._paginator.get_scrollfactor_pos_for_pageno(pageno)
514 scrollval = math.ceil(v_upper * scrollfactor)
515 self._view.scroll_to(scrollval)
516
517 def _paginate(self):
518 filelist = []
519 for i in self._epub._navmap.get_flattoc():
520 filelist.append(os.path.join(self._epub._tempdir, i))
521 # init files info
522 self._filelist = filelist
523 self._paginator = _Paginator(filelist)
524 self._paginator.connect('paginated', self._paginated_cb)
525
526 def get_filelist(self):
527 return self._filelist
528
529 def get_tempdir(self):
530 return self._epub._tempdir
531
532 def _load_next_page(self):
533 self._load_page(self._loaded_page + 1)
534
535 def _load_prev_page(self):
536 self._load_page(self._loaded_page - 1)
537
538 def _on_page_changed(self, oldpage, pageno):
539 if oldpage == pageno:
540 return
541 self.__page_changed = True
542 self._loaded_page = pageno
543 self._scrollbar.handler_block(self._scrollbar_change_value_cb_id)
544 self._scrollbar.set_value(pageno)
545 self._scrollbar.handler_unblock(self._scrollbar_change_value_cb_id)
546 # the indexes in read activity are zero based
547 self.emit('page-changed', (oldpage - 1), (pageno - 1))
548
549 def _load_page(self, pageno):
550 if pageno > self._pagecount or pageno < 1:
551 # TODO: Cause an exception
552 return
553 if self._loaded_page == pageno:
554 return
555
556 oldpage = self._loaded_page
557
558 filename = self._paginator.get_file_for_pageno(pageno)
559 filename = filename.replace('file://', '')
560
561 if filename != self._loaded_filename:
562 self._loaded_filename = filename
563
564 """
565 TODO: disabled because javascript can't be executed
566 with the velocity needed
567 # Copy javascript to highligth text to speech
568 destpath, destname = os.path.split(filename.replace('file://', ''))
569 shutil.copy('./epubview/highlight_words.js', destpath)
570 self._insert_js_reference(filename.replace('file://', ''),
571 destpath)
572 IMPORTANT: Find a way to do this without modify the files
573 now text highlight is implemented and the epub file is saved
574 """
575
576 self._view.stop_loading()
577 if filename.endswith('xml'):
578 dest = filename.replace('xml', 'xhtml')
579 if not os.path.exists(dest):
580 os.symlink(filename, dest)
581 self._view.load_uri('file://' + dest)
582 else:
583 self._view.load_uri('file://' + filename)
584 else:
585 self._loaded_page = pageno
586 self._scroll_page()
587 self._on_page_changed(oldpage, pageno)
588
589 def _insert_js_reference(self, file_name, path):
590 js_reference = '<script type="text/javascript" ' + \
591 'src="./highlight_words.js"></script>'
592 o = open(file_name + '.tmp', 'a')
593 for line in open(file_name):
594 line = line.replace('</head>', js_reference + '</head>')
595 o.write(line + "\n")
596 o.close()
597 shutil.copy(file_name + '.tmp', file_name)
598
599 def _load_file(self, path):
600 self._internal_link = None
601 if path.find('#') > -1:
602 self._internal_link = path[path.find('#'):]
603 path = path[:path.find('#')]
604
605 for filepath in self._filelist:
606 if filepath.endswith(path):
607 self._view.load_uri('file://' + filepath)
608 oldpage = self._loaded_page
609 self._loaded_page = \
610 self._paginator.get_base_pageno_for_file(filepath)
611 self._scroll_page()
612 self._on_page_changed(oldpage, self._loaded_page)
613 break
614
615 def _scrollbar_change_value_cb(self, range, scrolltype, value):
616 if scrolltype == Gtk.ScrollType.STEP_FORWARD or \
617 scrolltype == Gtk.ScrollType.STEP_BACKWARD:
618 self.scroll(scrolltype, False)
619 elif scrolltype == Gtk.ScrollType.JUMP or \
620 scrolltype == Gtk.ScrollType.PAGE_FORWARD or \
621 scrolltype == Gtk.ScrollType.PAGE_BACKWARD:
622 if value > self._scrollbar.props.adjustment.props.upper:
623 self._load_page(self._pagecount)
624 else:
625 self._load_page(int(value))
626 else:
627 print('Warning: unknown scrolltype %s with value %f' \
628 % (str(scrolltype), value))
629
630 # FIXME: This should not be needed here
631 self._scrollbar.set_value(self._loaded_page)
632
633 if self.__page_changed:
634 self.__page_changed = False
635 return False
636 else:
637 return True
638
639 def _paginated_cb(self, object):
640 self._ready = True
641
642 self._pagecount = self._paginator.get_total_pagecount()
643 self._scrollbar.set_range(1.0, self._pagecount)
644 self._scrollbar.set_increments(1.0, 1.0)
645 self._view.grab_focus()
646 self._view.grab_default()
647
648 def _destroy_cb(self, widget):
649 self._epub.close()
0 var parentElement;
1 var actualChild;
2 var actualWord;
3 var words;
4 var originalNode = null;
5 var modifiedNode = null;
6
7 function trim(s) {
8 s = ( s || '' ).replace( /^\s+|\s+$/g, '' );
9 return s.replace(/[\n\r\t]/g,' ');
10 }
11
12
13 function init() {
14 parentElement = document.getElementsByTagName("body")[0];
15 actualChild = new Array();
16 actualWord = 0;
17 actualChild.push(0);
18 }
19
20 function highLightNextWordInt() {
21 var nodeList = parentElement.childNodes;
22 ini_posi = actualChild[actualChild.length - 1];
23 for (var i=ini_posi; i < nodeList.length; i++) {
24 var node = nodeList[i];
25 if ((node.nodeName == "#text") && (trim(node.nodeValue) != '')) {
26 node_text = trim(node.nodeValue);
27 words = node_text.split(" ");
28 if (actualWord < words.length) {
29 originalNode = document.createTextNode(node.nodeValue);
30
31 prev_text = '';
32 for (var p1 = 0; p1 < actualWord; p1++) {
33 prev_text = prev_text + words[p1] + " ";
34 }
35 var textNode1 = document.createTextNode(prev_text);
36 var textNode2 = document.createTextNode(words[actualWord]+" ");
37 post_text = '';
38 for (var p2 = actualWord + 1; p2 < words.length; p2++) {
39 post_text = post_text + words[p2] + " ";
40 }
41 var textNode3 = document.createTextNode(post_text);
42 var newParagraph = document.createElement('p');
43 var boldNode = document.createElement('b');
44 boldNode.appendChild(textNode2);
45 newParagraph.appendChild(textNode1);
46 newParagraph.appendChild(boldNode);
47 newParagraph.appendChild(textNode3);
48
49 parentElement.insertBefore(newParagraph, node);
50 parentElement.removeChild(node);
51 modifiedNode = newParagraph;
52
53 actualWord = actualWord + 1;
54 if (actualWord >= words.length) {
55 actualChild.pop();
56 actualChild[actualChild.length - 1] = actualChild[actualChild.length - 1] + 2;
57 actualWord = 0;
58 parentElement = parentElement.parentNode;
59 }
60 }
61 throw "exit";
62 } else {
63 if (node.childNodes.length > 0) {
64 parentElement = node;
65 actualChild.push(0);
66 actualWord = 0;
67 highLightNextWordInt();
68 actualChild.pop();
69 }
70 }
71 }
72 return;
73 }
74
75
76 function highLightNextWord() {
77 if (typeof parentElement == "undefined") {
78 init();
79 }
80 if (originalNode != null) {
81 modifiedNode.parentNode.insertBefore(originalNode, modifiedNode);
82 modifiedNode.parentNode.removeChild(modifiedNode);
83 }
84 try {
85 highLightNextWordInt();
86 } catch(er) {
87 }
88 }
0 # Copyright 2009 One Laptop Per Child
1 # Author: Sayamindu Dasgupta <sayamindu@laptop.org>
2 # WebKit2 port Copyright (C) 2018 Lubomir Rintel <lkundrak@v3.sk>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import gi
19 gi.require_version('WebKit2', '4.0')
20
21 from gi.repository import GObject
22 from gi.repository import Gtk
23 from gi.repository import Gdk
24 from gi.repository import WebKit2
25 from . import widgets
26 import math
27 import os.path
28 import xml.etree.ElementTree as etree
29 import html.entities as html_entities
30
31 import threading
32
33 PAGE_WIDTH = 135
34 PAGE_HEIGHT = 216
35
36
37 def _pixel_to_mm(pixel, dpi):
38 inches = pixel / dpi
39 return int(inches / 0.03937)
40
41
42 def _mm_to_pixel(mm, dpi):
43 inches = mm * 0.03937
44 return int(inches * dpi)
45
46
47 class SearchThread(threading.Thread):
48
49 def __init__(self, obj):
50 threading.Thread.__init__(self)
51 self.obj = obj
52 self.stopthread = threading.Event()
53
54 def _start_search(self):
55 for entry in self.obj.flattoc:
56 if self.stopthread.isSet():
57 break
58 filepath = os.path.join(self.obj._document.get_basedir(), entry)
59 f = open(filepath)
60 if self._searchfile(f):
61 self.obj._matchfilelist.append(entry)
62 f.close()
63
64 self.obj._finished = True
65 GObject.idle_add(self.obj.emit, 'updated')
66
67 return False
68
69 def _searchfile(self, fileobj):
70 parser = etree.XMLParser(html=1)
71 for name, codepoint in html_entities.name2codepoint.items():
72 parser.entity[name] = chr(codepoint)
73 tree = etree.parse(fileobj, parser=parser)
74 root = tree.getroot()
75
76 body = None
77 for child in root:
78 if child.tag.endswith('body'):
79 body = child
80
81 if body is None:
82 return False
83
84 for child in body.iter():
85 if child.text is not None:
86 if child.text.lower().find(self.obj._text.lower()) > -1:
87 return True
88
89 return False
90
91 def run(self):
92 self._start_search()
93
94 def stop(self):
95 self.stopthread.set()
96
97
98 class _JobPaginator(GObject.GObject):
99
100 __gsignals__ = {
101 'paginated': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ([])),
102 }
103
104 def __init__(self, filelist):
105 GObject.GObject.__init__(self)
106
107 self._filelist = filelist
108 self._filedict = {}
109 self._pagemap = {}
110
111 self._bookheight = 0
112 self._count = 0
113 self._pagecount = 0
114
115 # TODO
116 """
117 self._screen = Gdk.Screen.get_default()
118 self._old_fontoptions = self._screen.get_font_options()
119 options = cairo.FontOptions()
120 options.set_hint_style(cairo.HINT_STYLE_MEDIUM)
121 options.set_antialias(cairo.ANTIALIAS_GRAY)
122 options.set_subpixel_order(cairo.SUBPIXEL_ORDER_DEFAULT)
123 options.set_hint_metrics(cairo.HINT_METRICS_DEFAULT)
124 self._screen.set_font_options(options)
125 """
126
127 self._temp_win = Gtk.Window()
128 self._temp_view = widgets._WebView()
129
130 settings = self._temp_view.get_settings()
131 settings.props.default_font_family = 'DejaVu LGC Serif'
132 settings.props.sans_serif_font_family = 'DejaVu LGC Sans'
133 settings.props.serif_font_family = 'DejaVu LGC Serif'
134 settings.props.monospace_font_family = 'DejaVu LGC Sans Mono'
135 # FIXME: This does not seem to work
136 # settings.props.auto_shrink_images = False
137 settings.props.enable_plugins = False
138 settings.props.default_font_size = 16
139 settings.props.default_monospace_font_size = 13
140 settings.props.default_charset = 'utf-8'
141
142 self._dpi = Gdk.Screen.get_default().get_resolution()
143 self._single_page_height = _mm_to_pixel(PAGE_HEIGHT, self._dpi)
144 self._temp_view.set_size_request(_mm_to_pixel(PAGE_WIDTH, self._dpi), self._single_page_height)
145
146 self._temp_win.add(self._temp_view)
147 self._temp_view.connect('load-changed', self._page_load_changed_cb)
148
149 self._temp_win.show_all()
150 self._temp_win.unmap()
151
152 self._temp_view.load_uri('file://' + self._filelist[self._count])
153
154 def get_single_page_height(self):
155 """
156 Returns the height in pixels of a single page
157 """
158 return self._single_page_height
159
160 def get_next_filename(self, actual_filename):
161 for n in range(len(self._filelist)):
162 filename = self._filelist[n]
163 if filename == actual_filename:
164 if n < len(self._filelist):
165 return self._filelist[n + 1]
166 return None
167
168 def _page_load_changed_cb(self, v, load_event):
169 if load_event != WebKit2.LoadEvent.FINISHED:
170 return True
171
172 pageheight = v.get_page_height()
173
174 if pageheight <= self._single_page_height:
175 pages = 1
176 else:
177 pages = pageheight / float(self._single_page_height)
178 for i in range(1, int(math.ceil(pages) + 1)):
179 if pages - i < 0:
180 pagelen = (pages - math.floor(pages)) / pages
181 else:
182 pagelen = 1 / pages
183 self._pagemap[float(self._pagecount + i)] = \
184 (v.get_uri(), (i - 1) / math.ceil(pages), pagelen)
185
186 self._pagecount += int(math.ceil(pages))
187 self._filedict[v.get_uri().replace('file://', '')] = \
188 (math.ceil(pages), math.ceil(pages) - pages)
189 self._bookheight += pageheight
190
191 if self._count + 1 >= len(self._filelist):
192 # TODO
193 # self._screen.set_font_options(self._old_fontoptions)
194 self.emit('paginated')
195 GObject.idle_add(self._cleanup)
196
197 else:
198 self._count += 1
199 self._temp_view.load_uri('file://' + self._filelist[self._count])
200
201 def _cleanup(self):
202 self._temp_win.destroy()
203
204 def get_file_for_pageno(self, pageno):
205 '''
206 Returns the file in which pageno occurs
207 '''
208 return self._pagemap[pageno][0]
209
210 def get_scrollfactor_pos_for_pageno(self, pageno):
211 '''
212 Returns the position scrollfactor (fraction) for pageno
213 '''
214 return self._pagemap[pageno][1]
215
216 def get_scrollfactor_len_for_pageno(self, pageno):
217 '''
218 Returns the length scrollfactor (fraction) for pageno
219 '''
220 return self._pagemap[pageno][2]
221
222 def get_pagecount_for_file(self, filename):
223 '''
224 Returns the number of pages in file
225 '''
226 return self._filedict[filename][0]
227
228 def get_base_pageno_for_file(self, filename):
229 '''
230 Returns the pageno which begins in filename
231 '''
232 for key in list(self._pagemap.keys()):
233 if self._pagemap[key][0].replace('file://', '') == filename:
234 return key
235
236 return None
237
238 def get_remfactor_for_file(self, filename):
239 '''
240 Returns the remainder
241 factor (1 - fraction length of last page in file)
242 '''
243 return self._filedict[filename][1]
244
245 def get_total_pagecount(self):
246 '''
247 Returns the total pagecount for the Epub file
248 '''
249 return self._pagecount
250
251 def get_total_height(self):
252 '''
253 Returns the total height of the Epub in pixels
254 '''
255 return self._bookheight
256
257
258 class _JobFind(GObject.GObject):
259 __gsignals__ = {
260 'updated': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ([])),
261 }
262
263 def __init__(self, document, start_page, n_pages, text,
264 case_sensitive=False):
265 """
266 Only case_sensitive=False is implemented
267 """
268 GObject.GObject.__init__(self)
269
270 self._finished = False
271 self._document = document
272 self._start_page = start_page
273 self._n_pages = n_pages
274 self._text = text
275 self._case_sensitive = case_sensitive
276 self.flattoc = self._document.get_flattoc()
277 self._matchfilelist = []
278 self._current_file_index = 0
279 self.threads = []
280
281 s_thread = SearchThread(self)
282 self.threads.append(s_thread)
283 s_thread.start()
284
285 def cancel(self):
286 '''
287 Cancels the search job
288 '''
289 for s_thread in self.threads:
290 s_thread.stop()
291
292 def is_finished(self):
293 '''
294 Returns True if the entire search job has been finished
295 '''
296 return self._finished
297
298 def get_next_file(self):
299 '''
300 Returns the next file which has the search pattern
301 '''
302 self._current_file_index += 1
303 try:
304 path = self._matchfilelist[self._current_file_index]
305 except IndexError:
306 self._current_file_index = 0
307 path = self._matchfilelist[self._current_file_index]
308
309 return path
310
311 def get_prev_file(self):
312 '''
313 Returns the previous file which has the search pattern
314 '''
315 self._current_file_index -= 1
316 try:
317 path = self._matchfilelist[self._current_file_index]
318 except IndexError:
319 self._current_file_index = -1
320 path = self._matchfilelist[self._current_file_index]
321
322 return path
323
324 def get_search_text(self):
325 '''
326 Returns the search text
327 '''
328 return self._text
329
330 def get_flags(self, forward=True):
331 '''
332 Returns the search flags
333 '''
334 flags = WebKit2.FindOptions.NONE
335 if self._case_sensitive:
336 flags = flags | WebKit2.FindOptions.CASE_INSENSITIVE
337 if not forward:
338 flags = flags | WebKit2.FindOptions.BACKWARDS
339 return flags
0 import xml.etree.ElementTree as etree
1 from gi.repository import Gtk
2
3
4 class NavPoint(object):
5
6 def __init__(self, label, contentsrc, children=[]):
7 self._label = label
8 self._contentsrc = contentsrc
9 self._children = children
10
11 def get_label(self):
12 return self._label
13
14 def get_contentsrc(self):
15 return self._contentsrc
16
17 def get_children(self):
18 return self._children
19
20
21 class NavMap(object):
22 def __init__(self, opffile, ncxfile, basepath):
23 self._basepath = basepath
24 self._opffile = opffile
25 self._tree = etree.parse(ncxfile)
26 self._root = self._tree.getroot()
27 self._gtktreestore = Gtk.TreeStore(str, str)
28 self._flattoc = []
29
30 self._populate_flattoc()
31 self._populate_toc()
32
33 def _populate_flattoc(self):
34 tree = etree.parse(self._opffile)
35 root = tree.getroot()
36
37 itemmap = {}
38 manifest = root.find('.//{http://www.idpf.org/2007/opf}manifest')
39 for element in manifest.iterfind('{http://www.idpf.org/2007/opf}item'):
40 itemmap[element.get('id')] = element
41
42 spine = root.find('.//{http://www.idpf.org/2007/opf}spine')
43 for element in spine.iterfind('{http://www.idpf.org/2007/opf}itemref'):
44 idref = element.get('idref')
45 href = itemmap[idref].get('href')
46 self._flattoc.append(self._basepath + href)
47
48 self._opffile.close()
49
50 def _populate_toc(self):
51 navmap = self._root.find(
52 '{http://www.daisy.org/z3986/2005/ncx/}navMap')
53 for navpoint in navmap.iterfind(
54 './{http://www.daisy.org/z3986/2005/ncx/}navPoint'):
55 self._process_navpoint(navpoint)
56
57 def _gettitle(self, navpoint):
58 text = navpoint.find(
59 './{http://www.daisy.org/z3986/2005/ncx/}' +
60 'navLabel/{http://www.daisy.org/z3986/2005/ncx/}text')
61 return text.text
62
63 def _getcontent(self, navpoint):
64 text = navpoint.find(
65 './{http://www.daisy.org/z3986/2005/ncx/}content')
66 if text is not None:
67 return self._basepath + text.get('src')
68 else:
69 return ""
70
71 def _process_navpoint(self, navpoint, parent=None):
72 title = self._gettitle(navpoint)
73 content = self._getcontent(navpoint)
74
75 # print title, content
76
77 iter = self._gtktreestore.append(parent, [title, content])
78 # self._flattoc.append((title, content))
79
80 childnavpointlist = list(navpoint.iterfind(
81 './{http://www.daisy.org/z3986/2005/ncx/}navPoint'))
82
83 if len(childnavpointlist):
84 for childnavpoint in childnavpointlist:
85 self._process_navpoint(childnavpoint, parent=iter)
86 else:
87 return
88
89 def get_gtktreestore(self):
90 '''
91 Returns a GtkTreeModel representation of the
92 Epub table of contents
93 '''
94 return self._gtktreestore
95
96 def get_flattoc(self):
97 '''
98 Returns a flat (linear) list of files to be
99 rendered.
100 '''
101 return self._flattoc
0 import logging
1
2 import gi
3 gi.require_version('WebKit2', '4.0')
4 gi.require_version('Gtk', '3.0')
5
6 from gi.repository import GLib
7 from gi.repository import WebKit2
8 from gi.repository import Gtk
9 from gi.repository import Gdk
10 from gi.repository import GObject
11
12 class _WebView(WebKit2.WebView):
13
14 __gsignals__ = {
15 'touch-change-page': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
16 ([bool])),
17 'scrolled': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
18 ([float])),
19 'scrolled-top': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
20 ([])),
21 'scrolled-bottom': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
22 ([])),
23 'selection-changed': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
24 ([bool])),
25 }
26
27 def __init__(self, **kwargs):
28 cm = WebKit2.UserContentManager()
29
30 cm.register_script_message_handler('scrolled');
31 cm.connect('script-message-received::scrolled',
32 lambda cm, result: self.emit('scrolled',
33 result.get_js_value().to_double()))
34
35 cm.register_script_message_handler('scrolled_top');
36 cm.connect('script-message-received::scrolled_top',
37 lambda cm, result: self.emit('scrolled-top'))
38
39 cm.register_script_message_handler('scrolled_bottom');
40 cm.connect('script-message-received::scrolled_bottom',
41 lambda cm, result: self.emit('scrolled-bottom'))
42
43 cm.register_script_message_handler('selection_changed');
44 cm.connect('script-message-received::selection_changed',
45 lambda cm, result: self.emit('selection-changed',
46 result.get_js_value().to_boolean()))
47
48 cm.add_script(WebKit2.UserScript('''
49 window.addEventListener("scroll", function(){
50 var handler = window.webkit.messageHandlers.scrolled;
51 handler.postMessage(window.scrollY);
52 });
53 document.addEventListener("selectionchange", function() {
54 var handler = window.webkit.messageHandlers.selection_changed;
55 handler.postMessage(window.getSelection() != '');
56 });
57 ''',
58 WebKit2.UserContentInjectedFrames.ALL_FRAMES,
59 WebKit2.UserScriptInjectionTime.START, None, None))
60
61 cm.add_style_sheet(WebKit2.UserStyleSheet('''
62 html { margin: 50px; }
63 body { overflow: hidden; }
64 ''',
65 WebKit2.UserContentInjectedFrames.ALL_FRAMES,
66 WebKit2.UserStyleLevel.USER, None, None))
67
68 WebKit2.WebView.__init__(self, user_content_manager=cm, **kwargs)
69 self.get_settings().set_enable_write_console_messages_to_stdout(True)
70
71 def do_context_menu (self, context_menu, event, hit_test_result):
72 # nope nope nope nopenopenopenenope
73 return True
74
75 def setup_touch(self):
76 self.get_window().set_events(
77 self.get_window().get_events() | Gdk.EventMask.TOUCH_MASK)
78 self.connect('event', self.__event_cb)
79
80 def __event_cb(self, widget, event):
81 if event.type == Gdk.EventType.TOUCH_BEGIN:
82 x = event.touch.x
83 view_width = widget.get_allocation().width
84 if x > view_width * 3 / 4:
85 self.emit('touch-change-page', True)
86 elif x < view_width * 1 / 4:
87 self.emit('touch-change-page', False)
88
89 def _execute_script_sync(self, js):
90 '''
91 This sad function aims to provide synchronous script execution like
92 WebKit-1.0's WebView.execute_script() to ease porting.
93 '''
94 res = ["0"]
95
96 def callback(self, task, user_data):
97 Gtk.main_quit()
98 result = self.run_javascript_finish(task)
99 if result is not None:
100 res[0] = result.get_js_value().to_string()
101
102 self.run_javascript(js, None, callback, None)
103 Gtk.main()
104 return res[0]
105
106 def get_page_height(self):
107 '''
108 Gets height (in pixels) of loaded (X)HTML page.
109 This is done via javascript at the moment
110 '''
111 return int(self._execute_script_sync('''
112 (function(){
113 if (document.body == null) {
114 return 0;
115 } else {
116 return Math.max(document.body.scrollHeight,
117 document.body.offsetHeight,
118 document.documentElement.clientHeight,
119 document.documentElement.scrollHeight,
120 document.documentElement.offsetHeight);
121 };
122 })()
123 '''))
124
125 def add_bottom_padding(self, incr):
126 '''
127 Adds incr pixels of margin to the end of the loaded (X)HTML page.
128 '''
129 self.run_javascript('document.body.style.marginBottom = "%dpx";' % (incr + 50))
130
131 def highlight_next_word(self):
132 '''
133 Highlight next word (for text to speech)
134 '''
135 self.run_javascript('highLightNextWord();')
136
137 def go_to_link(self, id_link):
138 self.run_javascript('window.location.href = "%s";' % id_link)
139
140 def get_vertical_position_element(self, id_link):
141 '''
142 Get the vertical position of a element, in pixels
143 '''
144 # remove the first '#' char
145 id_link = id_link[1:]
146 return int(self._execute_script_sync('''
147 (function(id_link){
148 var obj = document.getElementById(id_link);
149 var top = 0;
150 if (obj.offsetParent) {
151 while(1) {
152 top += obj.offsetTop;
153 if (!obj.offsetParent) {
154 break;
155 };
156 obj = obj.offsetParent;
157 };
158 } else if (obj.y) {
159 top += obj.y;
160 }
161 return top;
162 })("%s")
163 ''' % id_link))
164
165 def scroll_to(self, to):
166 '''
167 Set the vertical position in a document to a value in pixels.
168 '''
169 self.run_javascript('window.scrollTo(-1, %d);' % to)
170
171 def scroll_by(self, by):
172 '''
173 Modify the vertical position in a document by a value in pixels.
174 '''
175 self.run_javascript('''
176 (function(by){
177 var before = window.scrollY;
178 window.scrollBy(0, by);
179 if (window.scrollY == before) {
180 if (by < 0) {
181 var handler = window.webkit.messageHandlers.scrolled_top;
182 handler.postMessage(window.scrollY);
183 } else if (by > 0) {
184 var handler = window.webkit.messageHandlers.scrolled_bottom;
185 handler.postMessage(window.scrollY);
186 }
187 }
188 }(%d))
189 ''' % by)
5454 try:
5555 self._document = \
5656 EvinceDocument.Document.factory_get_document(file_path)
57 except GObject.GError, e:
57 except GObject.GError as e:
5858 _logger.error('Can not load document: %s', e)
5959 return
6060 else:
9393 'icon-color': profile.get_color().to_string(),
9494 'mime_type': 'text/uri-list', }
9595
96 for k, v in metadata.items():
96 for k, v in list(metadata.items()):
9797 jobject.metadata[k] = v
9898 file_path = os.path.join(get_activity_root(),
9999 'instance', '%i_' % time.time())
100100 open(file_path, 'w').write(url + '\r\n')
101 os.chmod(file_path, 0755)
101 os.chmod(file_path, 0o755)
102102 jobject.set_file_path(file_path)
103103 datastore.write(jobject)
104104 show_object_in_journal(jobject.object_id)
363363 Gtk.ScrollType.STEP_BACKWARD, Gtk.ScrollType.STEP_FORWARD,
364364 Gtk.ScrollType.START and Gtk.ScrollType.END
365365 '''
366 _logger.error('scroll: %s', scrolltype)
366 _logger.debug('scroll: %s', scrolltype)
367367
368368 if scrolltype == Gtk.ScrollType.PAGE_BACKWARD:
369369 self._view.scroll(Gtk.ScrollType.PAGE_BACKWARD, horizontal)
378378 elif scrolltype == Gtk.ScrollType.END:
379379 self.set_current_page(self._document.get_n_pages())
380380 else:
381 print ('Got unsupported scrolltype %s' % str(scrolltype))
381 print('Got unsupported scrolltype %s' % str(scrolltype))
382382
383383 def _scroll_step(self, forward, horizontal):
384384 if horizontal:
0 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
1 <!-- Created with Inkscape (http://www.inkscape.org/) -->
2
3 <svg
4 xmlns:dc="http://purl.org/dc/elements/1.1/"
5 xmlns:cc="http://creativecommons.org/ns#"
6 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
7 xmlns:svg="http://www.w3.org/2000/svg"
8 xmlns="http://www.w3.org/2000/svg"
9 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
10 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
11 width="32px"
12 height="32px"
13 id="svg2985"
14 version="1.1"
15 inkscape:version="0.48.3.1 r9886"
16 sodipodi:docname="dark-theme.svg">
17 <defs
18 id="defs2987" />
19 <sodipodi:namedview
20 id="base"
21 pagecolor="#ffffff"
22 bordercolor="#666666"
23 borderopacity="1.0"
24 inkscape:pageopacity="0.0"
25 inkscape:pageshadow="2"
26 inkscape:zoom="12.219689"
27 inkscape:cx="12.682313"
28 inkscape:cy="14.416406"
29 inkscape:current-layer="layer1"
30 showgrid="true"
31 inkscape:grid-bbox="true"
32 inkscape:document-units="px"
33 inkscape:snap-global="false"
34 objecttolerance="10000"
35 guidetolerance="10000"
36 showguides="false"
37 inkscape:window-width="1360"
38 inkscape:window-height="712"
39 inkscape:window-x="0"
40 inkscape:window-y="27"
41 inkscape:window-maximized="1">
42 <sodipodi:guide
43 position="0,0"
44 orientation="0,32"
45 id="guide3767" />
46 <sodipodi:guide
47 position="32,0"
48 orientation="-32,0"
49 id="guide3769" />
50 <sodipodi:guide
51 position="32,32"
52 orientation="0,-32"
53 id="guide3771" />
54 <sodipodi:guide
55 position="0,32"
56 orientation="32,0"
57 id="guide3773" />
58 <inkscape:grid
59 type="xygrid"
60 id="grid3775"
61 empspacing="5"
62 visible="true"
63 enabled="true"
64 snapvisiblegridlinesonly="true" />
65 <sodipodi:guide
66 position="0,0"
67 orientation="0,32"
68 id="guide3777" />
69 <sodipodi:guide
70 position="32,0"
71 orientation="-32,0"
72 id="guide3779" />
73 <sodipodi:guide
74 position="32,32"
75 orientation="0,-32"
76 id="guide3781" />
77 <sodipodi:guide
78 position="0,32"
79 orientation="32,0"
80 id="guide3783" />
81 <sodipodi:guide
82 position="0,0"
83 orientation="0,32"
84 id="guide3785" />
85 <sodipodi:guide
86 position="32,0"
87 orientation="-32,0"
88 id="guide3787" />
89 <sodipodi:guide
90 position="32,32"
91 orientation="0,-32"
92 id="guide3789" />
93 <sodipodi:guide
94 position="0,32"
95 orientation="32,0"
96 id="guide3791" />
97 </sodipodi:namedview>
98 <metadata
99 id="metadata2990">
100 <rdf:RDF>
101 <cc:Work
102 rdf:about="">
103 <dc:format>image/svg+xml</dc:format>
104 <dc:type
105 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
106 <dc:title></dc:title>
107 </cc:Work>
108 </rdf:RDF>
109 </metadata>
110 <g
111 id="layer1"
112 inkscape:label="Layer 1"
113 inkscape:groupmode="layer">
114 <rect
115 style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
116 id="rect2993"
117 width="31.022896"
118 height="31.194761"
119 x="0.66299194"
120 y="0.60228604"
121 ry="2.9258621" />
122 <text
123 xml:space="preserve"
124 style="font-size:11.23703003px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Sans"
125 x="5.8393445"
126 y="26.455677"
127 id="text3763"
128 sodipodi:linespacing="125%"><tspan
129 sodipodi:role="line"
130 id="tspan3765"
131 x="5.8393445"
132 y="26.455677"
133 style="font-size:30.90183258px;fill:#ffffff;fill-opacity:1">A</tspan></text>
134 </g>
135 </svg>
0 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
1 <!-- Created with Inkscape (http://www.inkscape.org/) -->
2
3 <svg
4 xmlns:dc="http://purl.org/dc/elements/1.1/"
5 xmlns:cc="http://creativecommons.org/ns#"
6 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
7 xmlns:svg="http://www.w3.org/2000/svg"
8 xmlns="http://www.w3.org/2000/svg"
9 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
10 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
11 width="32px"
12 height="32px"
13 id="svg2985"
14 version="1.1"
15 inkscape:version="0.48.3.1 r9886"
16 sodipodi:docname="dark-theme.svg">
17 <defs
18 id="defs2987" />
19 <sodipodi:namedview
20 id="base"
21 pagecolor="#ffffff"
22 bordercolor="#666666"
23 borderopacity="1.0"
24 inkscape:pageopacity="0.0"
25 inkscape:pageshadow="2"
26 inkscape:zoom="12.219689"
27 inkscape:cx="12.682313"
28 inkscape:cy="14.416406"
29 inkscape:current-layer="layer1"
30 showgrid="true"
31 inkscape:grid-bbox="true"
32 inkscape:document-units="px"
33 inkscape:snap-global="false"
34 objecttolerance="10000"
35 guidetolerance="10000"
36 showguides="false"
37 inkscape:window-width="1360"
38 inkscape:window-height="712"
39 inkscape:window-x="0"
40 inkscape:window-y="27"
41 inkscape:window-maximized="1">
42 <sodipodi:guide
43 position="0,0"
44 orientation="0,32"
45 id="guide3767" />
46 <sodipodi:guide
47 position="32,0"
48 orientation="-32,0"
49 id="guide3769" />
50 <sodipodi:guide
51 position="32,32"
52 orientation="0,-32"
53 id="guide3771" />
54 <sodipodi:guide
55 position="0,32"
56 orientation="32,0"
57 id="guide3773" />
58 <inkscape:grid
59 type="xygrid"
60 id="grid3775"
61 empspacing="5"
62 visible="true"
63 enabled="true"
64 snapvisiblegridlinesonly="true" />
65 <sodipodi:guide
66 position="0,0"
67 orientation="0,32"
68 id="guide3777" />
69 <sodipodi:guide
70 position="32,0"
71 orientation="-32,0"
72 id="guide3779" />
73 <sodipodi:guide
74 position="32,32"
75 orientation="0,-32"
76 id="guide3781" />
77 <sodipodi:guide
78 position="0,32"
79 orientation="32,0"
80 id="guide3783" />
81 <sodipodi:guide
82 position="0,0"
83 orientation="0,32"
84 id="guide3785" />
85 <sodipodi:guide
86 position="32,0"
87 orientation="-32,0"
88 id="guide3787" />
89 <sodipodi:guide
90 position="32,32"
91 orientation="0,-32"
92 id="guide3789" />
93 <sodipodi:guide
94 position="0,32"
95 orientation="32,0"
96 id="guide3791" />
97 </sodipodi:namedview>
98 <metadata
99 id="metadata2990">
100 <rdf:RDF>
101 <cc:Work
102 rdf:about="">
103 <dc:format>image/svg+xml</dc:format>
104 <dc:type
105 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
106 <dc:title></dc:title>
107 </cc:Work>
108 </rdf:RDF>
109 </metadata>
110 <g
111 id="layer1"
112 inkscape:label="Layer 1"
113 inkscape:groupmode="layer">
114 <rect
115 style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
116 id="rect2993"
117 width="31.022896"
118 height="31.194761"
119 x="0.66299194"
120 y="0.60228604"
121 ry="2.9258621" />
122 <text
123 xml:space="preserve"
124 style="font-size:11.23703002999999967px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
125 x="5.8393445"
126 y="26.455677"
127 id="text3763"
128 sodipodi:linespacing="125%"><tspan
129 sodipodi:role="line"
130 id="tspan3765"
131 x="5.8393445"
132 y="26.455677"
133 style="font-size:30.90183258000000066px;fill:#000000;fill-opacity:1">A</tspan></text>
134 </g>
135 </svg>
1818 from gi.repository import Gdk
1919 from gi.repository import GObject
2020
21 import StringIO
21 import io
2222 import cairo
2323 from gettext import gettext as _
2424
4141 # Color read from the Journal may be Unicode, but Rsvg needs
4242 # it as single byte string:
4343 self._color = color
44 if isinstance(color, unicode):
44 if isinstance(color, str):
4545 self._color = str(color)
4646 self._have_preview = False
4747 if buf is not None:
6060 stroke = self._color.split(',')[0]
6161 self._have_preview = True
6262 img = Gtk.Image()
63 str_buf = StringIO.StringIO(buf)
63 str_buf = io.BytesIO(buf)
6464 thumb_surface = cairo.ImageSurface.create_from_png(str_buf)
6565
6666 bg_width, bg_height = style.zoom(120), style.zoom(110)
77 msgstr ""
88 "Project-Id-Version: PACKAGE VERSION\n"
99 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2018-04-02 15:53+1000\n"
10 "POT-Creation-Date: 2019-03-07 20:17+1100\n"
1111 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1212 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1313 "Language-Team: LANGUAGE <LL@li.org>\n"
6565 msgid "Remove"
6666 msgstr ""
6767
68 #: readactivity.py:379
68 #: readactivity.py:385
6969 msgid "Please wait"
7070 msgstr ""
7171
72 #: readactivity.py:380
72 #: readactivity.py:386
7373 msgid "Starting connection..."
7474 msgstr ""
7575
76 #: readactivity.py:388
76 #: readactivity.py:394
7777 msgid "No book"
7878 msgstr ""
7979
80 #: readactivity.py:388
80 #: readactivity.py:394
8181 msgid "Choose something to read"
8282 msgstr ""
8383
84 #: readactivity.py:393
84 #: readactivity.py:399
8585 msgid "Back"
8686 msgstr ""
8787
88 #: readactivity.py:397
88 #: readactivity.py:403
8989 msgid "Previous page"
9090 msgstr ""
9191
92 #: readactivity.py:399
92 #: readactivity.py:405
9393 msgid "Previous bookmark"
9494 msgstr ""
9595
96 #: readactivity.py:411
96 #: readactivity.py:417
9797 msgid "Forward"
9898 msgstr ""
9999
100 #: readactivity.py:415
100 #: readactivity.py:421
101101 msgid "Next page"
102102 msgstr ""
103103
104 #: readactivity.py:417
104 #: readactivity.py:423
105105 msgid "Next bookmark"
106106 msgstr ""
107107
108 #: readactivity.py:468
108 #: readactivity.py:474
109109 msgid "Index"
110110 msgstr ""
111111
112 #: readactivity.py:564
112 #: readactivity.py:570
113113 msgid "Delete bookmark"
114114 msgstr ""
115115
116 #: readactivity.py:565
116 #: readactivity.py:571
117117 msgid "All the information related with this bookmark will be lost"
118118 msgstr ""
119119
120 #: readactivity.py:953
120 #: readactivity.py:959
121121 msgid "Receiving book..."
122122 msgstr ""
123123
124 #: readactivity.py:1027 readactivity.py:1222
125 #, python-format
126 msgid "%s (Page %d)"
124 #: readactivity.py:1036 readactivity.py:1229
125 #, python-format
126 msgid "%(title)s (Page %(number)d)"
127127 msgstr ""
128128
129129 #: readdialog.py:52
206206 msgid "Rotate right"
207207 msgstr ""
208208
209 #: readtoolbar.py:270 readtoolbar.py:348
210 msgid "Inverted Colors"
211 msgstr ""
212
209213 #: readtoolbar.py:324
210214 msgid "Show Tray"
211215 msgstr ""
214218 msgid "Hide Tray"
215219 msgstr ""
216220
217 #: speechtoolbar.py:55
221 #: readtoolbar.py:345
222 msgid "Normal Colors"
223 msgstr ""
224
225 #: speechtoolbar.py:65
218226 msgid "Play / Pause"
219227 msgstr ""
220228
221 #: speechtoolbar.py:63
229 #: speechtoolbar.py:73
222230 msgid "Stop"
223231 msgstr ""
+0
-223
po/agr.po less more
0 # SOME DESCRIPTIVE TITLE.
1 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # This file is distributed under the same license as the PACKAGE package.
3 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 msgid ""
5 msgstr ""
6 "Project-Id-Version: PACKAGE VERSION\n"
7 "Report-Msgid-Bugs-To: \n"
8 "POT-Creation-Date: 2017-03-24 17:39+1100\n"
9 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
10 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
11 "Language-Team: LANGUAGE <LL@li.org>\n"
12 "Language: agr\n"
13 "MIME-Version: 1.0\n"
14 "Content-Type: text/plain; charset=UTF-8\n"
15 "Content-Transfer-Encoding: 8bit\n"
16 "X-Generator: Translate Toolkit 1.11.0\n"
17
18 #: activity/activity.info:2
19 msgid "Read"
20 msgstr ""
21
22 #: activity/activity.info:3
23 msgid ""
24 "Use this activity when you are ready to read! Remember to flip your computer "
25 "around to feel like you are really holding a book!"
26 msgstr ""
27
28 #: readdialog.py:52
29 msgid "Cancel"
30 msgstr ""
31
32 #: readdialog.py:58
33 msgid "Ok"
34 msgstr ""
35
36 #: readdialog.py:120
37 msgid "<b>Title</b>:"
38 msgstr ""
39
40 #: readdialog.py:147
41 msgid "<b>Author</b>:"
42 msgstr ""
43
44 #: readdialog.py:163
45 msgid "<b>Details</b>:"
46 msgstr ""
47
48 #: evinceadapter.py:92
49 msgid "URL from Read"
50 msgstr ""
51
52 #: speechtoolbar.py:57
53 msgid "Play / Pause"
54 msgstr ""
55
56 #: speechtoolbar.py:65
57 msgid "Stop"
58 msgstr ""
59
60 #: readactivity.py:379
61 msgid "Please wait"
62 msgstr ""
63
64 #: readactivity.py:380
65 msgid "Starting connection..."
66 msgstr ""
67
68 #: readactivity.py:388
69 msgid "No book"
70 msgstr ""
71
72 #: readactivity.py:388
73 msgid "Choose something to read"
74 msgstr ""
75
76 #: readactivity.py:393
77 msgid "Back"
78 msgstr ""
79
80 #: readactivity.py:397
81 msgid "Previous page"
82 msgstr ""
83
84 #: readactivity.py:399
85 msgid "Previous bookmark"
86 msgstr ""
87
88 #: readactivity.py:411
89 msgid "Forward"
90 msgstr ""
91
92 #: readactivity.py:415
93 msgid "Next page"
94 msgstr ""
95
96 #: readactivity.py:417
97 msgid "Next bookmark"
98 msgstr ""
99
100 #: readactivity.py:468
101 msgid "Index"
102 msgstr ""
103
104 #: readactivity.py:564
105 msgid "Delete bookmark"
106 msgstr ""
107
108 #: readactivity.py:565
109 msgid "All the information related with this bookmark will be lost"
110 msgstr ""
111
112 #: readactivity.py:954
113 msgid "Receiving book..."
114 msgstr ""
115
116 #: readactivity.py:1032 readactivity.py:1227
117 #, python-format
118 msgid "%s (Page %d)"
119 msgstr ""
120
121 #: readtoolbar.py:61
122 msgid "Previous"
123 msgstr ""
124
125 #: readtoolbar.py:68
126 msgid "Next"
127 msgstr ""
128
129 #: readtoolbar.py:79
130 msgid "Highlight"
131 msgstr ""
132
133 #: readtoolbar.py:160
134 msgid "Find first"
135 msgstr ""
136
137 #: readtoolbar.py:166
138 msgid "Find previous"
139 msgstr ""
140
141 #: readtoolbar.py:168
142 msgid "Find next"
143 msgstr ""
144
145 #: readtoolbar.py:188
146 msgid "Table of contents"
147 msgstr ""
148
149 #: readtoolbar.py:197
150 msgid "Zoom out"
151 msgstr ""
152
153 #: readtoolbar.py:203
154 msgid "Zoom in"
155 msgstr ""
156
157 #: readtoolbar.py:209
158 msgid "Zoom to width"
159 msgstr ""
160
161 #: readtoolbar.py:215
162 msgid "Zoom to fit"
163 msgstr ""
164
165 #: readtoolbar.py:221
166 msgid "Actual size"
167 msgstr ""
168
169 #: readtoolbar.py:232
170 msgid "Fullscreen"
171 msgstr ""
172
173 #: readtoolbar.py:252
174 msgid "Rotate left"
175 msgstr ""
176
177 #: readtoolbar.py:258
178 msgid "Rotate right"
179 msgstr ""
180
181 #: readtoolbar.py:324
182 msgid "Show Tray"
183 msgstr ""
184
185 #: readtoolbar.py:326
186 msgid "Hide Tray"
187 msgstr ""
188
189 #: linkbutton.py:133
190 msgid "Go to Bookmark"
191 msgstr ""
192
193 #: linkbutton.py:139
194 msgid "Remove"
195 msgstr ""
196
197 #: bookmarkview.py:107
198 #, python-format
199 msgid "Bookmark added by %(user)s %(time)s"
200 msgstr ""
201
202 #: bookmarkview.py:143 bookmarkview.py:192
203 msgid "Add notes for bookmark: "
204 msgstr ""
205
206 #: bookmarkview.py:188
207 #, python-format
208 msgid "%s's bookmark"
209 msgstr ""
210
211 #: bookmarkview.py:189
212 #, python-format
213 msgid "Bookmark for page %d"
214 msgstr ""
215
216 #: comicadapter.py:69
217 msgid "Can not read Comic Book Archive"
218 msgstr ""
219
220 #: comicadapter.py:70
221 msgid "No readable images were found"
222 msgstr ""
+0
-203
po/bi.po less more
0 # SOME DESCRIPTIVE TITLE.
1 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # This file is distributed under the same license as the PACKAGE package.
3 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 # SOME DESCRIPTIVE TITLE.
5 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
6 # This file is distributed under the same license as the PACKAGE package.
7 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
8 # SOME DESCRIPTIVE TITLE.
9 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
10 # This file is distributed under the same license as the PACKAGE package.
11 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
12 #, fuzzy
13 msgid ""
14 msgstr ""
15 "Project-Id-Version: PACKAGE VERSION\n"
16 "Report-Msgid-Bugs-To: \n"
17 "POT-Creation-Date: 2013-04-11 00:31-0400\n"
18 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
19 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
20 "Language-Team: LANGUAGE <LL@li.org>\n"
21 "Language: \n"
22 "MIME-Version: 1.0\n"
23 "Content-Type: text/plain; charset=UTF-8\n"
24 "Content-Transfer-Encoding: 8bit\n"
25 "X-Generator: Translate Toolkit 1.1.1rc4\n"
26
27 #. TRANS: "name" option from activity.info file
28 msgid "Read"
29 msgstr ""
30
31 #. TRANS: "summary" option from activity.info file
32 #. TRANS: "description" option from activity.info file
33 msgid ""
34 "Use this activity when you are ready to read! Remember to flip your computer "
35 "around to feel like you are really holding a book!"
36 msgstr ""
37
38 #. TRANS: This goes like Bookmark added by User 5 days ago
39 #. TRANS: (the elapsed string gets translated automatically)
40 #: bookmarkview.py:110
41 #, python-format
42 msgid "Bookmark added by %(user)s %(time)s"
43 msgstr ""
44
45 #: bookmarkview.py:153 bookmarkview.py:204
46 msgid "Add notes for bookmark: "
47 msgstr ""
48
49 #: bookmarkview.py:200
50 #, python-format
51 msgid "%s's bookmark"
52 msgstr ""
53
54 #: bookmarkview.py:201
55 #, python-format
56 msgid "Bookmark for page %d"
57 msgstr ""
58
59 #: evinceadapter.py:88
60 msgid "URL from Read"
61 msgstr ""
62
63 #: linkbutton.py:94
64 msgid "Go to Bookmark"
65 msgstr ""
66
67 #: linkbutton.py:99
68 msgid "Remove"
69 msgstr ""
70
71 #: readactivity.py:333
72 msgid "No book"
73 msgstr ""
74
75 #: readactivity.py:333
76 msgid "Choose something to read"
77 msgstr ""
78
79 #: readactivity.py:338
80 msgid "Back"
81 msgstr ""
82
83 #: readactivity.py:342
84 msgid "Previous page"
85 msgstr ""
86
87 #: readactivity.py:344
88 msgid "Previous bookmark"
89 msgstr ""
90
91 #: readactivity.py:356
92 msgid "Forward"
93 msgstr ""
94
95 #: readactivity.py:360
96 msgid "Next page"
97 msgstr ""
98
99 #: readactivity.py:362
100 msgid "Next bookmark"
101 msgstr ""
102
103 #: readactivity.py:413
104 msgid "Index"
105 msgstr ""
106
107 #: readactivity.py:534
108 msgid "Delete bookmark"
109 msgstr ""
110
111 #: readactivity.py:535
112 msgid "All the information related with this bookmark will be lost"
113 msgstr ""
114
115 #: readactivity.py:895 readactivity.py:1090
116 #, python-format
117 msgid "Page %d"
118 msgstr ""
119
120 #: readdialog.py:52
121 msgid "Cancel"
122 msgstr ""
123
124 #: readdialog.py:58
125 msgid "Ok"
126 msgstr ""
127
128 #: readdialog.py:116
129 msgid "<b>Title</b>:"
130 msgstr ""
131
132 #: readdialog.py:142
133 msgid "<b>Details</b>:"
134 msgstr ""
135
136 #: readtoolbar.py:61
137 msgid "Previous"
138 msgstr ""
139
140 #: readtoolbar.py:68
141 msgid "Next"
142 msgstr ""
143
144 #: readtoolbar.py:79
145 msgid "Highlight"
146 msgstr ""
147
148 #: readtoolbar.py:160
149 msgid "Find first"
150 msgstr ""
151
152 #: readtoolbar.py:166
153 msgid "Find previous"
154 msgstr ""
155
156 #: readtoolbar.py:168
157 msgid "Find next"
158 msgstr ""
159
160 #: readtoolbar.py:189
161 msgid "Table of contents"
162 msgstr ""
163
164 #: readtoolbar.py:198
165 msgid "Zoom out"
166 msgstr ""
167
168 #: readtoolbar.py:204
169 msgid "Zoom in"
170 msgstr ""
171
172 #: readtoolbar.py:210
173 msgid "Zoom to width"
174 msgstr ""
175
176 #: readtoolbar.py:216
177 msgid "Zoom to fit"
178 msgstr ""
179
180 #: readtoolbar.py:222
181 msgid "Actual size"
182 msgstr ""
183
184 #: readtoolbar.py:233
185 msgid "Fullscreen"
186 msgstr ""
187
188 #: readtoolbar.py:301
189 msgid "Show Tray"
190 msgstr ""
191
192 #: readtoolbar.py:303
193 msgid "Hide Tray"
194 msgstr ""
195
196 #: speechtoolbar.py:57
197 msgid "Play / Pause"
198 msgstr ""
199
200 #: speechtoolbar.py:65
201 msgid "Stop"
202 msgstr ""
+0
-203
po/cpp.po less more
0 # SOME DESCRIPTIVE TITLE.
1 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # This file is distributed under the same license as the PACKAGE package.
3 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 # SOME DESCRIPTIVE TITLE.
5 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
6 # This file is distributed under the same license as the PACKAGE package.
7 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
8 # SOME DESCRIPTIVE TITLE.
9 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
10 # This file is distributed under the same license as the PACKAGE package.
11 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
12 #, fuzzy
13 msgid ""
14 msgstr ""
15 "Project-Id-Version: PACKAGE VERSION\n"
16 "Report-Msgid-Bugs-To: \n"
17 "POT-Creation-Date: 2013-04-11 00:31-0400\n"
18 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
19 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
20 "Language-Team: LANGUAGE <LL@li.org>\n"
21 "Language: \n"
22 "MIME-Version: 1.0\n"
23 "Content-Type: text/plain; charset=UTF-8\n"
24 "Content-Transfer-Encoding: 8bit\n"
25 "X-Generator: Translate Toolkit 1.1.1rc4\n"
26
27 #. TRANS: "name" option from activity.info file
28 msgid "Read"
29 msgstr ""
30
31 #. TRANS: "summary" option from activity.info file
32 #. TRANS: "description" option from activity.info file
33 msgid ""
34 "Use this activity when you are ready to read! Remember to flip your computer "
35 "around to feel like you are really holding a book!"
36 msgstr ""
37
38 #. TRANS: This goes like Bookmark added by User 5 days ago
39 #. TRANS: (the elapsed string gets translated automatically)
40 #: bookmarkview.py:110
41 #, python-format
42 msgid "Bookmark added by %(user)s %(time)s"
43 msgstr ""
44
45 #: bookmarkview.py:153 bookmarkview.py:204
46 msgid "Add notes for bookmark: "
47 msgstr ""
48
49 #: bookmarkview.py:200
50 #, python-format
51 msgid "%s's bookmark"
52 msgstr ""
53
54 #: bookmarkview.py:201
55 #, python-format
56 msgid "Bookmark for page %d"
57 msgstr ""
58
59 #: evinceadapter.py:88
60 msgid "URL from Read"
61 msgstr ""
62
63 #: linkbutton.py:94
64 msgid "Go to Bookmark"
65 msgstr ""
66
67 #: linkbutton.py:99
68 msgid "Remove"
69 msgstr ""
70
71 #: readactivity.py:333
72 msgid "No book"
73 msgstr ""
74
75 #: readactivity.py:333
76 msgid "Choose something to read"
77 msgstr ""
78
79 #: readactivity.py:338
80 msgid "Back"
81 msgstr ""
82
83 #: readactivity.py:342
84 msgid "Previous page"
85 msgstr ""
86
87 #: readactivity.py:344
88 msgid "Previous bookmark"
89 msgstr ""
90
91 #: readactivity.py:356
92 msgid "Forward"
93 msgstr ""
94
95 #: readactivity.py:360
96 msgid "Next page"
97 msgstr ""
98
99 #: readactivity.py:362
100 msgid "Next bookmark"
101 msgstr ""
102
103 #: readactivity.py:413
104 msgid "Index"
105 msgstr ""
106
107 #: readactivity.py:534
108 msgid "Delete bookmark"
109 msgstr ""
110
111 #: readactivity.py:535
112 msgid "All the information related with this bookmark will be lost"
113 msgstr ""
114
115 #: readactivity.py:895 readactivity.py:1090
116 #, python-format
117 msgid "Page %d"
118 msgstr ""
119
120 #: readdialog.py:52
121 msgid "Cancel"
122 msgstr ""
123
124 #: readdialog.py:58
125 msgid "Ok"
126 msgstr ""
127
128 #: readdialog.py:116
129 msgid "<b>Title</b>:"
130 msgstr ""
131
132 #: readdialog.py:142
133 msgid "<b>Details</b>:"
134 msgstr ""
135
136 #: readtoolbar.py:61
137 msgid "Previous"
138 msgstr ""
139
140 #: readtoolbar.py:68
141 msgid "Next"
142 msgstr ""
143
144 #: readtoolbar.py:79
145 msgid "Highlight"
146 msgstr ""
147
148 #: readtoolbar.py:160
149 msgid "Find first"
150 msgstr ""
151
152 #: readtoolbar.py:166
153 msgid "Find previous"
154 msgstr ""
155
156 #: readtoolbar.py:168
157 msgid "Find next"
158 msgstr ""
159
160 #: readtoolbar.py:189
161 msgid "Table of contents"
162 msgstr ""
163
164 #: readtoolbar.py:198
165 msgid "Zoom out"
166 msgstr ""
167
168 #: readtoolbar.py:204
169 msgid "Zoom in"
170 msgstr ""
171
172 #: readtoolbar.py:210
173 msgid "Zoom to width"
174 msgstr ""
175
176 #: readtoolbar.py:216
177 msgid "Zoom to fit"
178 msgstr ""
179
180 #: readtoolbar.py:222
181 msgid "Actual size"
182 msgstr ""
183
184 #: readtoolbar.py:233
185 msgid "Fullscreen"
186 msgstr ""
187
188 #: readtoolbar.py:301
189 msgid "Show Tray"
190 msgstr ""
191
192 #: readtoolbar.py:303
193 msgid "Hide Tray"
194 msgstr ""
195
196 #: speechtoolbar.py:57
197 msgid "Play / Pause"
198 msgstr ""
199
200 #: speechtoolbar.py:65
201 msgid "Stop"
202 msgstr ""
1414 "Project-Id-Version: PACKAGE VERSION\n"
1515 "Report-Msgid-Bugs-To: \n"
1616 "POT-Creation-Date: 2017-03-24 17:39+1100\n"
17 "PO-Revision-Date: 2017-11-03 09:26+0000\n"
18 "Last-Translator: dark159123 <r.j.hansen@protonmail.com>\n"
17 "PO-Revision-Date: 2018-10-22 18:39+0000\n"
18 "Last-Translator: scootergrisen <scootergrisen@gmail.com>\n"
1919 "Language-Team: LANGUAGE <LL@li.org>\n"
2020 "Language: da\n"
2121 "MIME-Version: 1.0\n"
2323 "Content-Transfer-Encoding: 8bit\n"
2424 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
2525 "X-Generator: Pootle 2.5.1.1\n"
26 "X-POOTLE-MTIME: 1509701219.000000\n"
26 "X-POOTLE-MTIME: 1540233554.000000\n"
2727
2828 #: activity/activity.info:2
2929 msgid "Read"
168168
169169 #: readtoolbar.py:209
170170 msgid "Zoom to width"
171 msgstr "Tilpas til bredde"
171 msgstr "Zoom til bredde"
172172
173173 #: readtoolbar.py:215
174174 msgid "Zoom to fit"
+0
-231
po/ff.po less more
0 # SOME DESCRIPTIVE TITLE.
1 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # This file is distributed under the same license as the PACKAGE package.
3 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 # SOME DESCRIPTIVE TITLE.
5 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
6 # This file is distributed under the same license as the PACKAGE package.
7 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
8 # SOME DESCRIPTIVE TITLE.
9 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
10 # This file is distributed under the same license as the PACKAGE package.
11 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
12 msgid ""
13 msgstr ""
14 "Project-Id-Version: PACKAGE VERSION\n"
15 "Report-Msgid-Bugs-To: \n"
16 "POT-Creation-Date: 2017-03-24 17:39+1100\n"
17 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
18 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
19 "Language-Team: LANGUAGE <LL@li.org>\n"
20 "Language: ff\n"
21 "MIME-Version: 1.0\n"
22 "Content-Type: text/plain; charset=UTF-8\n"
23 "Content-Transfer-Encoding: 8bit\n"
24 "X-Generator: Translate Toolkit 1.0.1\n"
25
26 #: activity/activity.info:2
27 msgid "Read"
28 msgstr ""
29
30 #: activity/activity.info:3
31 msgid ""
32 "Use this activity when you are ready to read! Remember to flip your computer "
33 "around to feel like you are really holding a book!"
34 msgstr ""
35
36 #: readdialog.py:52
37 msgid "Cancel"
38 msgstr ""
39
40 #: readdialog.py:58
41 msgid "Ok"
42 msgstr ""
43
44 #: readdialog.py:120
45 msgid "<b>Title</b>:"
46 msgstr ""
47
48 #: readdialog.py:147
49 msgid "<b>Author<