Codebase list gajim-urlimagepreview / 8623568
import version 2.4.4 Martin 4 years ago
7 changed file(s) with 541 addition(s) and 108 deletion(s). Raw diff Collapse all Expand all
00 <?xml version="1.0" encoding="UTF-8"?>
1 <!-- Generated with glade 3.22.1 -->
1 <!-- Generated with glade 3.22.2 -->
22 <interface>
33 <requires lib="gtk+" version="3.20"/>
44 <object class="GtkMenu" id="context_menu">
55 <property name="can_focus">False</property>
6 <child>
7 <object class="GtkMenuItem" id="download">
8 <property name="visible">True</property>
9 <property name="can_focus">False</property>
10 <property name="label" translatable="yes">_Download</property>
11 <property name="use_underline">True</property>
12 </object>
13 </child>
614 <child>
715 <object class="GtkMenuItem" id="open">
816 <property name="visible">True</property>
00 [info]
11 name: Url image preview
22 short_name: url_image_preview
3 version: 2.4.3
3 version: 2.4.4
44 description: Displays a preview of image links.
55 authors = Denis Fomin <fominde@gmail.com>
66 Yann Leboulanger <asterix@lagaule.org>
0 # This is an excerpt of Media Types from
1 # https://www.iana.org/assignments/media-types/media-types.xhtml
2 # plus some additions
3 MIME_TYPES = (
4 # application/
5 'application/calendar+json',
6 'application/calendar+xml',
7 'application/epub+zip',
8 'application/json',
9 'application/mp4',
10 'application/msword',
11 'application/octet-stream',
12 'application/ogg',
13 'application/pdf',
14 'application/pgp-encrypted',
15 'application/pgp-signature',
16 'application/postscript',
17 'application/rtf',
18 'application/vcard+json',
19 'application/vcard+xml',
20 'application/vnd.amazon.mobi8-ebook',
21 'application/vnd.google-earth.kml+xml',
22 'application/vnd.google-earth.kmz',
23 # Start office
24 'application/vnd.ms-access',
25 'application/vnd.ms-excel',
26 'application/vnd.ms-excel.addin.macroEnabled.12',
27 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
28 'application/vnd.ms-excel.sheet.macroEnabled.12',
29 'application/vnd.ms-excel.template.macroEnabled.12',
30 'application/vnd.ms-powerpoint',
31 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
32 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
33 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
34 'application/vnd.ms-powerpoint.template.macroEnabled.12',
35 'application/vnd.ms-word.document.macroEnabled.12',
36 'application/vnd.ms-word.template.macroEnabled.12',
37 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
38 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
39 'application/vnd.openxmlformats-officedocument.presentationml.template',
40 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
41 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
42 'application/vnd.openxmlformats-officedocument.vmlDrawing',
43 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
44 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
45 # End office
46 'application/vnd.sqlite3',
47 'application/zip',
48 # audio/*
49 'audio/aac',
50 'audio/ac3',
51 'audio/flac',
52 'audio/mp4',
53 'audio/mpeg',
54 'audio/ogg',
55 'audio/opus',
56 'audio/wav',
57 'audio/x-flac',
58 'audio/x-matroska',
59 # font/*
60 'font/ttf',
61 'font/woff',
62 'font/woff2',
63 # image/*
64 'image/bmp',
65 'image/x-bmp',
66 'image/x-ms-bmp',
67 'image/gif',
68 'image/heic',
69 'image/heif',
70 'image/jpeg',
71 'image/png',
72 'image/svg+xml',
73 'image/tiff',
74 'image/vnd.adobe.photoshop',
75 'image/vnd.dwg',
76 'image/vnd.dxf',
77 'image/vnd.microsoft.icon',
78 'image/x-icon',
79 'image/x-xcf',
80 # model/*
81 'model/mtl',
82 'model/obj',
83 'model/stl',
84 # text/*
85 'text/calendar',
86 'text/csv',
87 'text/markdown',
88 'text/rtf',
89 'text/vcard',
90 # video/*
91 'video/H264',
92 'video/H265',
93 'video/mp4',
94 'video/mpeg4-generic',
95 'video/ogg',
96 'video/quicktime',
97 'video/vc1',
98 'video/VP8',
99 'video/webm',
100 'video/x-matroska',
101 'video/x-msvideo',
102 )
0 .preview-box {
1 border: 1px solid;
2 border-color: @borders;
3 border-radius: 5px;
4 padding: 10px;
5 margin: 5px;
6 background-color: @theme_unfocused_base_color;
7 }
8 .preview-button {
9 border: 1px solid @borders;
10 }
0 <?xml version="1.0" encoding="UTF-8"?>
1 <!-- Generated with glade 3.22.2 -->
2 <interface>
3 <requires lib="gtk+" version="3.20"/>
4 <object class="GtkBox" id="preview_box">
5 <property name="visible">True</property>
6 <property name="can_focus">False</property>
7 <property name="orientation">vertical</property>
8 <property name="spacing">3</property>
9 <child>
10 <object class="GtkEventBox" id="event_box">
11 <property name="visible">True</property>
12 <property name="can_focus">False</property>
13 <property name="halign">start</property>
14 <child>
15 <placeholder/>
16 </child>
17 </object>
18 <packing>
19 <property name="expand">True</property>
20 <property name="fill">True</property>
21 <property name="position">0</property>
22 </packing>
23 </child>
24 <child>
25 <object class="GtkBox" id="button_box">
26 <property name="visible">True</property>
27 <property name="can_focus">False</property>
28 <property name="spacing">6</property>
29 <child>
30 <object class="GtkBox">
31 <property name="visible">True</property>
32 <property name="can_focus">False</property>
33 <property name="orientation">vertical</property>
34 <child>
35 <object class="GtkLabel" id="file_name">
36 <property name="visible">True</property>
37 <property name="can_focus">False</property>
38 <property name="selectable">True</property>
39 <property name="ellipsize">end</property>
40 <property name="single_line_mode">True</property>
41 <property name="xalign">0</property>
42 <style>
43 <class name="dim-label"/>
44 </style>
45 </object>
46 <packing>
47 <property name="expand">False</property>
48 <property name="fill">True</property>
49 <property name="position">0</property>
50 </packing>
51 </child>
52 <child>
53 <object class="GtkLabel" id="file_size">
54 <property name="visible">True</property>
55 <property name="can_focus">False</property>
56 <property name="single_line_mode">True</property>
57 <property name="xalign">0</property>
58 <style>
59 <class name="dim-label"/>
60 </style>
61 </object>
62 <packing>
63 <property name="expand">False</property>
64 <property name="fill">True</property>
65 <property name="position">1</property>
66 </packing>
67 </child>
68 </object>
69 <packing>
70 <property name="expand">False</property>
71 <property name="fill">True</property>
72 <property name="position">0</property>
73 </packing>
74 </child>
75 <child>
76 <object class="GtkButton" id="save_as_button">
77 <property name="visible">True</property>
78 <property name="can_focus">True</property>
79 <property name="receives_default">False</property>
80 <property name="tooltip_text" translatable="yes">Save as...</property>
81 <property name="valign">end</property>
82 <child>
83 <object class="GtkImage">
84 <property name="visible">True</property>
85 <property name="can_focus">False</property>
86 <property name="icon_name">document-save-as-symbolic</property>
87 </object>
88 </child>
89 <style>
90 <class name="preview-button"/>
91 <class name="flat"/>
92 </style>
93 </object>
94 <packing>
95 <property name="expand">False</property>
96 <property name="fill">True</property>
97 <property name="pack_type">end</property>
98 <property name="position">0</property>
99 </packing>
100 </child>
101 <child>
102 <object class="GtkButton" id="open_folder_button">
103 <property name="visible">True</property>
104 <property name="can_focus">True</property>
105 <property name="receives_default">False</property>
106 <property name="tooltip_text" translatable="yes">Open folder</property>
107 <property name="valign">end</property>
108 <child>
109 <object class="GtkImage">
110 <property name="visible">True</property>
111 <property name="can_focus">False</property>
112 <property name="icon_name">folder-symbolic</property>
113 </object>
114 </child>
115 <style>
116 <class name="preview-button"/>
117 <class name="flat"/>
118 </style>
119 </object>
120 <packing>
121 <property name="expand">False</property>
122 <property name="fill">True</property>
123 <property name="pack_type">end</property>
124 <property name="position">1</property>
125 </packing>
126 </child>
127 <child>
128 <object class="GtkButton" id="download_button">
129 <property name="visible">True</property>
130 <property name="can_focus">True</property>
131 <property name="receives_default">False</property>
132 <property name="tooltip_text" translatable="yes">Download</property>
133 <property name="valign">end</property>
134 <property name="relief">none</property>
135 <child>
136 <object class="GtkImage">
137 <property name="visible">True</property>
138 <property name="can_focus">False</property>
139 <property name="icon_name">folder-download-symbolic</property>
140 </object>
141 </child>
142 <style>
143 <class name="preview-button"/>
144 <class name="flat"/>
145 </style>
146 </object>
147 <packing>
148 <property name="expand">False</property>
149 <property name="fill">True</property>
150 <property name="pack_type">end</property>
151 <property name="position">3</property>
152 </packing>
153 </child>
154 </object>
155 <packing>
156 <property name="expand">False</property>
157 <property name="fill">True</property>
158 <property name="position">1</property>
159 </packing>
160 </child>
161 <style>
162 <class name="preview-box"/>
163 </style>
164 </object>
165 </interface>
1616 import os
1717 import logging
1818 import shutil
19 import mimetypes
1920 from pathlib import Path
2021 from functools import partial
2122 from urllib.parse import urlparse
2324
2425 from gi.repository import Gtk
2526 from gi.repository import Gdk
27 from gi.repository import GdkPixbuf
28 from gi.repository import Gio
2629 from gi.repository import GLib
2730 from gi.repository import Soup
28 from gi.repository import GdkPixbuf
2931
3032 from gajim.common import app
3133 from gajim.common import configpaths
3739 from gajim.common.helpers import get_user_proxy
3840 from gajim.gtk.dialogs import ErrorDialog
3941 from gajim.gtk.filechoosers import FileSaveDialog
42 from gajim.gtk.util import get_cursor
43 from gajim.gtk.util import get_monitor_scale_factor
4044 from gajim.gtk.util import load_icon
41 from gajim.gtk.util import get_monitor_scale_factor
4245
4346 from gajim.plugins import GajimPlugin
4447 from gajim.plugins.helpers import get_builder
4548 from gajim.plugins.plugins_i18n import _
4649
4750 from url_image_preview.config_dialog import UrlImagePreviewConfigDialog
48
51 from url_image_preview.mime_types import MIME_TYPES
4952
5053 log = logging.getLogger('gajim.p.preview')
5154
7174 from url_image_preview.utils import parse_fragment
7275 from url_image_preview.utils import create_thumbnail
7376 from url_image_preview.utils import pixbuf_from_data
74 from url_image_preview.utils import create_clickable_image
7577 from url_image_preview.utils import filename_from_uri
7678 # pylint: enable=ungrouped-imports
7779
78 def get_accepted_mime_types():
79 accepted_mime_types = set()
80
81 def get_previewable_mime_types():
82 previewable_mime_types = set()
8083 for fmt in GdkPixbuf.Pixbuf.get_formats():
8184 for mime_type in fmt.get_mime_types():
82 accepted_mime_types.add(mime_type.lower())
85 previewable_mime_types.add(mime_type.lower())
8386 if Image is not None:
8487 Image.init()
8588 for mime_type in Image.MIME.values():
86 accepted_mime_types.add(mime_type.lower())
89 previewable_mime_types.add(mime_type.lower())
8790 return tuple(filter(
8891 lambda mime_type: mime_type.startswith('image'),
89 accepted_mime_types
92 previewable_mime_types
9093 ))
9194
92 ACCEPTED_MIME_TYPES = get_accepted_mime_types()
93
95
96 PREVIEWABLE_MIME_TYPES = get_previewable_mime_types()
97 mime_types = set(MIME_TYPES)
98 # Merge both: if it’s a previewable image, it should be allowed
99 ALLOWED_MIME_TYPES = mime_types.union(PREVIEWABLE_MIME_TYPES)
94100
95101 class UrlImagePreviewPlugin(GajimPlugin):
96102 def init(self):
108114 self._on_disconnect_chat_control_base),
109115 'history_window': (self._on_connect_history_window,
110116 self._on_disconnect_history_window),
111 'print_real_text': (self._print_real_text, None), }
117 'print_real_text': (self._print_real_text, None),
118 }
112119
113120 self.config_default_values = {
114121 'PREVIEW_SIZE': (150, 'Preview size (100-1000)'),
115 'MAX_FILE_SIZE': (5242880, 'Max file size for image preview'),
122 'MAX_FILE_SIZE': ('10485760', 'Max file size for image preview'),
116123 'ALLOW_ALL_IMAGES': (False, ''),
117124 'LEFTCLICK_ACTION': ('open_menuitem', 'Open'),
118125 'ANONYMOUS_MUC': (False, ''),
119 'VERIFY': (True, ''),}
126 'VERIFY': (True, ''),
127 }
120128
121129 self._textviews = {}
122130 self._sessions = {}
130138 if GLib.mkdir_with_parents(str(self._thumb_dir), 0o700) != 0:
131139 log.error('Failed to create: %s', self._thumb_dir)
132140
141 if app.config.get('use_kib_mib'):
142 self._units = GLib.FormatSizeFlags.IEC_UNITS
143 else:
144 self._units = GLib.FormatSizeFlags.DEFAULT
145
133146 self._migrate_config()
147 self._load_css()
134148
135149 def _migrate_config(self):
136150 action = self.config['LEFTCLICK_ACTION']
137151 if action.endswith('_menuitem'):
138152 self.config['LEFTCLICK_ACTION'] = action[:-9]
153
154 @staticmethod
155 def _load_css():
156 path = Path(__file__).parent / 'preview.css'
157 try:
158 with path.open('r') as file:
159 css = file.read()
160 except Exception as exc:
161 log.error('Error loading css: %s', exc)
162 return
163
164 try:
165 provider = Gtk.CssProvider()
166 provider.load_from_data(bytes(css.encode('utf-8')))
167 Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
168 provider, 610)
169 except Exception:
170 log.exception('Error loading application css')
139171
140172 def _on_connect_chat_control_base(self, chat_control):
141173 account = chat_control.account
160192 if textview == textview_:
161193 return control_id
162194
163 def _create_session(self, account):
195 @staticmethod
196 def _create_session(account):
164197 session = Soup.Session()
165198 session.add_feature_by_type(Soup.ContentSniffer)
166199 session.props.https_aliases = ['aesgcm']
210243 size=preview.size,
211244 scale=get_monitor_scale_factor(),
212245 pixbuf=True)
213 self._update_textview(pixbuf, preview)
246 self._update_textview(preview, pixbuf)
214247 return
215248
216249 preview = self._process_web_uri(uri,
286319 account):
287320 try:
288321 split_geo_uri(uri)
289 except Exception as error:
322 except Exception as err:
290323 log.error(uri)
291 log.error(error)
324 log.error(err)
292325 return
293326
294327 return Preview(uri,
330363 log.error('%s: %s', preview.orig_path.name, error)
331364 return
332365
333 if preview.create_thumbnail(data):
334 write_file_async(preview.thumb_path,
335 preview.thumbnail,
336 self._on_thumb_write_finished,
337 preview)
366 preview.mime_type = self._guess_mime_type(preview.orig_path)
367 preview.file_size = os.path.getsize(preview.orig_path)
368 if preview.is_previewable:
369 if preview.create_thumbnail(data):
370 write_file_async(preview.thumb_path,
371 preview.thumbnail,
372 self._on_thumb_write_finished,
373 preview)
374 else:
375 self._update_textview(preview, None)
338376
339377 def _on_thumb_load_finished(self, data, error, preview):
340378 if data is None:
342380 return
343381
344382 preview.thumbnail = data
383 preview.mime_type = self._guess_mime_type(preview.orig_path)
384 preview.file_size = os.path.getsize(preview.orig_path)
345385
346386 try:
347387 pixbuf = pixbuf_from_data(preview.thumbnail)
350390 preview.thumb_path.name,
351391 err)
352392 return
353 self._update_textview(pixbuf, preview)
354
355 def _download_content(self, preview):
393 self._update_textview(preview, pixbuf)
394
395 def _download_content(self, preview, force=False):
356396 if preview.account is None:
357397 # History Window can be opened without account context
358398 # This means we can not apply proxy settings
360400 log.info('Start downloading: %s', preview.request_uri)
361401 message = Soup.Message.new('GET', preview.request_uri)
362402 message.connect('starting', self._check_certificate, preview)
363 message.connect('content-sniffed', self._on_content_sniffed, preview)
403 message.connect(
404 'content-sniffed', self._on_content_sniffed, preview, force)
364405
365406 session = self._get_session(preview.account)
366407 session.queue_message(message, self._on_finished, preview)
378419 session.cancel_message(message, Soup.Status.CANCELLED)
379420 return
380421
381 def _on_content_sniffed(self, message, type_, _params, preview):
382 size = message.props.response_headers.get_content_length()
422 def _on_content_sniffed(self, message, type_, _params, preview, force):
423 file_size = message.props.response_headers.get_content_length()
383424 uri = message.props.uri.to_string(False)
384425 session = self._get_session(preview.account)
385 if type_ not in ACCEPTED_MIME_TYPES:
386 log.info('Not allowed content type: %s, %s', type_, uri)
426 preview.mime_type = type_
427 preview.file_size = file_size
428
429 if type_ not in ALLOWED_MIME_TYPES:
430 log.info('Not an allowed content type: %s, %s', type_, uri)
387431 session.cancel_message(message, Soup.Status.CANCELLED)
388432 return
389433
390 if size == 0 or size > int(self.config['MAX_FILE_SIZE']):
434 if file_size == 0 or file_size > int(self.config['MAX_FILE_SIZE']):
391435 log.info('File size (%s) too big or unknown (zero) for URL: \'%s\'',
392 size, uri)
393 session.cancel_message(message, Soup.Status.CANCELLED)
394 return
436 file_size, uri)
437 if not force:
438 session.cancel_message(message, Soup.Status.CANCELLED)
439
440 self._update_textview(preview, None)
395441
396442 def _on_finished(self, _session, message, preview):
397443 if message.status_code != Soup.Status.OK:
411457 self._on_orig_write_finished,
412458 preview)
413459
414 if preview.create_thumbnail(data):
415 write_file_async(preview.thumb_path,
416 preview.thumbnail,
417 self._on_thumb_write_finished,
418 preview)
460 if preview.is_previewable:
461 if preview.create_thumbnail(data):
462 write_file_async(preview.thumb_path,
463 preview.thumbnail,
464 self._on_thumb_write_finished,
465 preview)
466 else:
467 self._update_textview(preview, None)
419468
420469 @staticmethod
421470 def _on_orig_write_finished(_result, error, preview):
424473 return
425474
426475 log.info('File stored: %s', preview.orig_path.name)
476 preview.file_size = os.path.getsize(preview.orig_path)
427477
428478 def _on_thumb_write_finished(self, _result, error, preview):
429479 if error is not None:
434484
435485 try:
436486 pixbuf = pixbuf_from_data(preview.thumbnail)
437 except Exception as err:
487 except Exception as error:
438488 log.error('Unable to load: %s, %s',
439489 preview.thumb_path.name,
440 err)
441 return
442 self._update_textview(pixbuf, preview)
443
444 def _update_textview(self, pixbuf, preview):
490 error)
491 return
492 self._update_textview(preview, pixbuf)
493
494 @staticmethod
495 def _guess_mime_type(data):
496 mime_type, _ = mimetypes.MimeTypes().guess_type(data)
497 if mime_type is None:
498 # Try to guess MIME type by file name
499 mime_type, _ = Gio.content_type_guess(str(data), None)
500 log.debug('Guessed MIME type: %s', str(mime_type))
501 return mime_type
502
503 @staticmethod
504 def _get_icon_for_mime_type(mime_type):
505 if mime_type is None:
506 return Gio.Icon.new_for_string('mail-attachment')
507 return Gio.content_type_get_icon(mime_type)
508
509 def _update_textview(self, preview, data):
445510 textview = self._textviews.get(preview.control_id)
446511 if textview is None:
447512 # Control closed
453518 anchor = buffer_.create_child_anchor(iter_)
454519 anchor.plaintext = preview.uri
455520
456 image = create_clickable_image(pixbuf, preview)
457
458 textview.tv.add_child_at_anchor(image, anchor)
521 preview_widget = self._create_preview_widget(preview, data)
522
523 textview.tv.add_child_at_anchor(preview_widget, anchor)
459524 buffer_.delete(iter_,
460525 buffer_.get_iter_at_mark(preview.end_mark))
461526
462 image.connect('button-press-event',
463 self._on_button_press_event,
464 preview)
465
466527 if textview.autoscroll:
467528 textview.scroll_to_end()
468529
530 def _create_preview_widget(self, preview, data):
531 if isinstance(data, GdkPixbuf.PixbufAnimation):
532 image = Gtk.Image.new_from_animation(data)
533 elif isinstance(data, GdkPixbuf.Pixbuf):
534 image = Gtk.Image.new_from_pixbuf(data)
535 else:
536 icon = self._get_icon_for_mime_type(preview.mime_type)
537 image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.DIALOG)
538
539 def _on_realize(box):
540 box.get_window().set_cursor(get_cursor('pointer'))
541
542 def _on_enter_leave(button, event):
543 if event.type == Gdk.EventType.ENTER_NOTIFY:
544 button.get_window().set_cursor(get_cursor('default'))
545 else:
546 button.get_window().set_cursor(get_cursor('text'))
547
548 path = self.local_file_path('preview.ui')
549 ui = get_builder(path)
550
551 ui.download_button.set_no_show_all(True)
552 ui.download_button.connect('enter-notify-event', _on_enter_leave)
553 ui.download_button.connect('leave-notify-event', _on_enter_leave)
554 ui.download_button.connect('clicked', self._on_download, preview)
555
556 ui.save_as_button.set_no_show_all(True)
557 ui.save_as_button.connect('enter-notify-event', _on_enter_leave)
558 ui.save_as_button.connect('leave-notify-event', _on_enter_leave)
559 ui.save_as_button.connect('clicked', self._on_save_as, preview)
560
561 ui.open_folder_button.set_no_show_all(True)
562 ui.open_folder_button.connect('enter-notify-event', _on_enter_leave)
563 ui.open_folder_button.connect('leave-notify-event', _on_enter_leave)
564 ui.open_folder_button.connect('clicked', self._on_open_folder, preview)
565
566 ui.event_box.set_tooltip_text(preview.filename)
567 ui.event_box.add(image)
568 ui.event_box.connect('realize', _on_realize)
569 ui.event_box.connect('button-press-event',
570 self._on_button_press_event,
571 preview)
572
573 ui.preview_box.show_all()
574
575 if preview.is_geo_uri:
576 ui.file_name.set_text(_('Click to view location'))
577 ui.save_as_button.hide()
578 ui.open_folder_button.hide()
579 ui.download_button.hide()
580 location = split_geo_uri(preview.uri)
581 ui.file_size.set_text(_('Lat: %s Lon: %s') % (
582 location.lat, location.lon))
583 ui.event_box.set_tooltip_text(_('Location at Lat: %s Lon: %s') % (
584 location.lat, location.lon))
585 ui.event_box.set_halign(Gtk.Align.CENTER)
586 ui.preview_box.set_size_request(160, -1)
587 return ui.preview_box
588
589 if preview.is_previewable and preview.orig_exists():
590 ui.event_box.set_halign(Gtk.Align.CENTER)
591 else:
592 image.set_property('pixel-size', 64)
593
594 if preview.orig_exists():
595 ui.download_button.hide()
596 else:
597 ui.save_as_button.hide()
598 ui.open_folder_button.hide()
599
600 file_size_string = _('File size unknown')
601 if preview.file_size != 0:
602 file_size_string = GLib.format_size_full(
603 preview.file_size, self._units)
604 ui.file_size.set_text(file_size_string)
605
606 ui.preview_box.set_size_request(300, -1)
607 ui.file_name.set_text(preview.filename)
608 ui.file_name.set_tooltip_text(preview.filename)
609
610 return ui.preview_box
611
469612 def _get_context_menu(self, preview):
613 def destroy(menu, _pspec):
614 visible = menu.get_property('visible')
615 if not visible:
616 GLib.idle_add(menu.destroy)
617
470618 path = self.local_file_path('context_menu.ui')
471619 ui = get_builder(path)
472 if preview.is_aes_encrypted:
473 ui.open_link_in_browser.hide()
474
475 if preview.is_geo_uri:
476 ui.open_link_in_browser.hide()
477 ui.save_as.hide()
478 ui.open_folder.hide()
479
620
621 ui.download.connect(
622 'activate', self._on_download, preview)
480623 ui.open.connect(
481624 'activate', self._on_open, preview)
482625 ui.save_as.connect(
487630 'activate', self._on_open_link_in_browser, preview)
488631 ui.copy_link_location.connect(
489632 'activate', self._on_copy_link_location, preview)
490
491 def destroy(menu, _pspec):
492 visible = menu.get_property('visible')
493 if not visible:
494 GLib.idle_add(menu.destroy)
495
496633 ui.context_menu.connect('notify::visible', destroy)
634
635 if preview.is_aes_encrypted:
636 ui.open_link_in_browser.hide()
637
638 if preview.is_geo_uri:
639 ui.download.hide()
640 ui.open_link_in_browser.hide()
641 ui.save_as.hide()
642 ui.open_folder.hide()
643 return ui.context_menu
644
645 if preview.orig_exists():
646 ui.download.hide()
647 else:
648 ui.open.hide()
649 ui.save_as.hide()
650 ui.open_folder.hide()
651
497652 return ui.context_menu
498653
499 @staticmethod
500 def _on_open(_menu, preview):
654 def _on_download(self, _menu, preview):
655 if not preview.orig_exists():
656 self._download_content(preview, force=True)
657
658 def _on_open(self, _menu, preview):
501659 if preview.is_geo_uri:
502660 open_uri(preview.uri)
503661 return
662
663 if not preview.orig_exists():
664 self._download_content(preview, force=True)
665 return
666
504667 open_file(preview.orig_path)
505668
506 @staticmethod
507 def _on_save_as(_menu, preview):
669 def _on_save_as(self, _menu, preview):
508670 def on_ok(target_path):
509671 dirname = Path(target_path).parent
510672 if not os.access(dirname, os.W_OK):
516678 return
517679 shutil.copyfile(str(preview.orig_path), target_path)
518680
681 if not preview.orig_exists():
682 self._download_content(preview, force=True)
683 return
684
519685 FileSaveDialog(on_ok,
520686 path=app.config.get('last_save_dir'),
521687 file_name=preview.filename,
522688 transient_for=app.app.get_active_window())
523689
524 @staticmethod
525 def _on_open_folder(_menu, preview):
690 def _on_open_folder(self, _menu, preview):
691 if not preview.orig_exists():
692 self._download_content(preview, force=True)
693 return
526694 open_file(preview.orig_path.parent)
527695
528696 @staticmethod
559727 self._uri = uri
560728 self._urlparts = urlparts
561729 self._filename = filename_from_uri(self._uri)
730
562731 self.size = size
563732 self.control_id = control_id
564733 self.orig_path = orig_path
565734 self.thumb_path = thumb_path
566735 self.start_mark = start_mark
567736 self.end_mark = end_mark
737 self.account = account
738
568739 self.thumbnail = None
569 self.account = account
740 self.mime_type = None
741 self.file_size = 0
570742
571743 self.key, self.iv = None, None
572744 if self.is_aes_encrypted:
579751 @property
580752 def is_web_uri(self):
581753 return not self.is_geo_uri
754
755 @property
756 def is_previewable(self):
757 return self.mime_type in PREVIEWABLE_MIME_TYPES
582758
583759 @property
584760 def uri(self):
611787 def create_thumbnail(self, data):
612788 self.thumbnail = create_thumbnail(data, self.size)
613789 if self.thumbnail is None:
614 log.warning('creating thumbnail failed for: %s', self.orig_path)
790 log.warning('Creating thumbnail failed for: %s', self.orig_path)
615791 return False
616792 return True
2525
2626 from gi.repository import GdkPixbuf
2727 from gi.repository import GLib
28 from gi.repository import Gtk
2928
3029 from PIL import Image
3130
3433 from cryptography.hazmat.primitives.ciphers import algorithms
3534 from cryptography.hazmat.primitives.ciphers.modes import GCM
3635
37 from gajim.gtk.util import get_cursor
3836
3937 log = logging.getLogger('gajim.p.preview.utils')
4038
236234 return loader.get_pixbuf()
237235
238236
239 def create_clickable_image(pixbuf, preview):
240 if isinstance(pixbuf, GdkPixbuf.PixbufAnimation):
241 image = Gtk.Image.new_from_animation(pixbuf)
242 else:
243 image = Gtk.Image.new_from_pixbuf(pixbuf)
244
245 css = '''#Preview {
246 box-shadow: 0px 0px 3px 0px alpha(@theme_text_color, 0.2);
247 margin: 5px 10px 5px 10px; }'''
248
249 provider = Gtk.CssProvider()
250 provider.load_from_data(bytes(css.encode()))
251 context = image.get_style_context()
252 context.add_provider(provider,
253 Gtk.STYLE_PROVIDER_PRIORITY_USER)
254
255 image.set_name('Preview')
256
257 def _on_realize(box):
258 box.get_window().set_cursor(get_cursor('pointer'))
259
260 event_box = Gtk.EventBox()
261 event_box.connect('realize', _on_realize)
262 event_box.set_tooltip_text(preview.uri)
263 event_box.add(image)
264 event_box.show_all()
265 return event_box
266
267
268237 def parse_fragment(fragment):
269238 if not fragment:
270239 raise ValueError('Invalid fragment')