New upstream version 123
Jonas Smedegaard
3 years ago
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 | ||
0 | 12 | 121 |
1 | 13 | |
2 | 14 | * Add README.md (Rudra Sadhu), |
1 | 1 | name = Read |
2 | 2 | bundle_id = org.laptop.sugar.ReadActivity |
3 | 3 | 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 | |
7 | 7 | max_participants = 10 |
8 | 8 | license = GPLv2+;LGPLv2+ |
9 | 9 | 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 | |
11 | 11 | url = https://help.sugarlabs.org/en/read.html |
12 | 12 | 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> |
104 | 104 | # TRANS: (the elapsed string gets translated automatically) |
105 | 105 | tooltip_footer = ( |
106 | 106 | _('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}) | |
109 | 109 | |
110 | 110 | l = Gtk.Label('<big>%s</big>' % tooltip_header) |
111 | 111 | l.set_use_markup(True) |
195 | 195 | dialog.show_all() |
196 | 196 | |
197 | 197 | 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)) | |
199 | 199 | self.update_for_page(page) |
200 | 200 | |
201 | 201 | 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) |
54 | 54 | try: |
55 | 55 | self._document = \ |
56 | 56 | EvinceDocument.Document.factory_get_document(file_path) |
57 | except GObject.GError, e: | |
57 | except GObject.GError as e: | |
58 | 58 | _logger.error('Can not load document: %s', e) |
59 | 59 | return |
60 | 60 | else: |
93 | 93 | 'icon-color': profile.get_color().to_string(), |
94 | 94 | 'mime_type': 'text/uri-list', } |
95 | 95 | |
96 | for k, v in metadata.items(): | |
96 | for k, v in list(metadata.items()): | |
97 | 97 | jobject.metadata[k] = v |
98 | 98 | file_path = os.path.join(get_activity_root(), |
99 | 99 | 'instance', '%i_' % time.time()) |
100 | 100 | open(file_path, 'w').write(url + '\r\n') |
101 | os.chmod(file_path, 0755) | |
101 | os.chmod(file_path, 0o755) | |
102 | 102 | jobject.set_file_path(file_path) |
103 | 103 | datastore.write(jobject) |
104 | 104 | show_object_in_journal(jobject.object_id) |
363 | 363 | Gtk.ScrollType.STEP_BACKWARD, Gtk.ScrollType.STEP_FORWARD, |
364 | 364 | Gtk.ScrollType.START and Gtk.ScrollType.END |
365 | 365 | ''' |
366 | _logger.error('scroll: %s', scrolltype) | |
366 | _logger.debug('scroll: %s', scrolltype) | |
367 | 367 | |
368 | 368 | if scrolltype == Gtk.ScrollType.PAGE_BACKWARD: |
369 | 369 | self._view.scroll(Gtk.ScrollType.PAGE_BACKWARD, horizontal) |
378 | 378 | elif scrolltype == Gtk.ScrollType.END: |
379 | 379 | self.set_current_page(self._document.get_n_pages()) |
380 | 380 | else: |
381 | print ('Got unsupported scrolltype %s' % str(scrolltype)) | |
381 | print('Got unsupported scrolltype %s' % str(scrolltype)) | |
382 | 382 | |
383 | 383 | def _scroll_step(self, forward, horizontal): |
384 | 384 | 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> |
18 | 18 | from gi.repository import Gdk |
19 | 19 | from gi.repository import GObject |
20 | 20 | |
21 | import StringIO | |
21 | import io | |
22 | 22 | import cairo |
23 | 23 | from gettext import gettext as _ |
24 | 24 | |
41 | 41 | # Color read from the Journal may be Unicode, but Rsvg needs |
42 | 42 | # it as single byte string: |
43 | 43 | self._color = color |
44 | if isinstance(color, unicode): | |
44 | if isinstance(color, str): | |
45 | 45 | self._color = str(color) |
46 | 46 | self._have_preview = False |
47 | 47 | if buf is not None: |
60 | 60 | stroke = self._color.split(',')[0] |
61 | 61 | self._have_preview = True |
62 | 62 | img = Gtk.Image() |
63 | str_buf = StringIO.StringIO(buf) | |
63 | str_buf = io.BytesIO(buf) | |
64 | 64 | thumb_surface = cairo.ImageSurface.create_from_png(str_buf) |
65 | 65 | |
66 | 66 | bg_width, bg_height = style.zoom(120), style.zoom(110) |
7 | 7 | msgstr "" |
8 | 8 | "Project-Id-Version: PACKAGE VERSION\n" |
9 | 9 | "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" | |
11 | 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
12 | 12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
13 | 13 | "Language-Team: LANGUAGE <LL@li.org>\n" |
65 | 65 | msgid "Remove" |
66 | 66 | msgstr "" |
67 | 67 | |
68 | #: readactivity.py:379 | |
68 | #: readactivity.py:385 | |
69 | 69 | msgid "Please wait" |
70 | 70 | msgstr "" |
71 | 71 | |
72 | #: readactivity.py:380 | |
72 | #: readactivity.py:386 | |
73 | 73 | msgid "Starting connection..." |
74 | 74 | msgstr "" |
75 | 75 | |
76 | #: readactivity.py:388 | |
76 | #: readactivity.py:394 | |
77 | 77 | msgid "No book" |
78 | 78 | msgstr "" |
79 | 79 | |
80 | #: readactivity.py:388 | |
80 | #: readactivity.py:394 | |
81 | 81 | msgid "Choose something to read" |
82 | 82 | msgstr "" |
83 | 83 | |
84 | #: readactivity.py:393 | |
84 | #: readactivity.py:399 | |
85 | 85 | msgid "Back" |
86 | 86 | msgstr "" |
87 | 87 | |
88 | #: readactivity.py:397 | |
88 | #: readactivity.py:403 | |
89 | 89 | msgid "Previous page" |
90 | 90 | msgstr "" |
91 | 91 | |
92 | #: readactivity.py:399 | |
92 | #: readactivity.py:405 | |
93 | 93 | msgid "Previous bookmark" |
94 | 94 | msgstr "" |
95 | 95 | |
96 | #: readactivity.py:411 | |
96 | #: readactivity.py:417 | |
97 | 97 | msgid "Forward" |
98 | 98 | msgstr "" |
99 | 99 | |
100 | #: readactivity.py:415 | |
100 | #: readactivity.py:421 | |
101 | 101 | msgid "Next page" |
102 | 102 | msgstr "" |
103 | 103 | |
104 | #: readactivity.py:417 | |
104 | #: readactivity.py:423 | |
105 | 105 | msgid "Next bookmark" |
106 | 106 | msgstr "" |
107 | 107 | |
108 | #: readactivity.py:468 | |
108 | #: readactivity.py:474 | |
109 | 109 | msgid "Index" |
110 | 110 | msgstr "" |
111 | 111 | |
112 | #: readactivity.py:564 | |
112 | #: readactivity.py:570 | |
113 | 113 | msgid "Delete bookmark" |
114 | 114 | msgstr "" |
115 | 115 | |
116 | #: readactivity.py:565 | |
116 | #: readactivity.py:571 | |
117 | 117 | msgid "All the information related with this bookmark will be lost" |
118 | 118 | msgstr "" |
119 | 119 | |
120 | #: readactivity.py:953 | |
120 | #: readactivity.py:959 | |
121 | 121 | msgid "Receiving book..." |
122 | 122 | msgstr "" |
123 | 123 | |
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)" | |
127 | 127 | msgstr "" |
128 | 128 | |
129 | 129 | #: readdialog.py:52 |
206 | 206 | msgid "Rotate right" |
207 | 207 | msgstr "" |
208 | 208 | |
209 | #: readtoolbar.py:270 readtoolbar.py:348 | |
210 | msgid "Inverted Colors" | |
211 | msgstr "" | |
212 | ||
209 | 213 | #: readtoolbar.py:324 |
210 | 214 | msgid "Show Tray" |
211 | 215 | msgstr "" |
214 | 218 | msgid "Hide Tray" |
215 | 219 | msgstr "" |
216 | 220 | |
217 | #: speechtoolbar.py:55 | |
221 | #: readtoolbar.py:345 | |
222 | msgid "Normal Colors" | |
223 | msgstr "" | |
224 | ||
225 | #: speechtoolbar.py:65 | |
218 | 226 | msgid "Play / Pause" |
219 | 227 | msgstr "" |
220 | 228 | |
221 | #: speechtoolbar.py:63 | |
229 | #: speechtoolbar.py:73 | |
222 | 230 | msgid "Stop" |
223 | 231 | msgstr "" |
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 | # 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 | # 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 "" |
14 | 14 | "Project-Id-Version: PACKAGE VERSION\n" |
15 | 15 | "Report-Msgid-Bugs-To: \n" |
16 | 16 | "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" | |
19 | 19 | "Language-Team: LANGUAGE <LL@li.org>\n" |
20 | 20 | "Language: da\n" |
21 | 21 | "MIME-Version: 1.0\n" |
23 | 23 | "Content-Transfer-Encoding: 8bit\n" |
24 | 24 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" |
25 | 25 | "X-Generator: Pootle 2.5.1.1\n" |
26 | "X-POOTLE-MTIME: 1509701219.000000\n" | |
26 | "X-POOTLE-MTIME: 1540233554.000000\n" | |
27 | 27 | |
28 | 28 | #: activity/activity.info:2 |
29 | 29 | msgid "Read" |
168 | 168 | |
169 | 169 | #: readtoolbar.py:209 |
170 | 170 | msgid "Zoom to width" |
171 | msgstr "Tilpas til bredde" | |
171 | msgstr "Zoom til bredde" | |
172 | 172 | |
173 | 173 | #: readtoolbar.py:215 |
174 | 174 | msgid "Zoom to fit" |
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< |