aboutsummaryrefslogtreecommitdiff
path: root/main/src/ui/conversation_content_view/file_image_widget.vala
diff options
context:
space:
mode:
Diffstat (limited to 'main/src/ui/conversation_content_view/file_image_widget.vala')
-rw-r--r--main/src/ui/conversation_content_view/file_image_widget.vala253
1 files changed, 214 insertions, 39 deletions
diff --git a/main/src/ui/conversation_content_view/file_image_widget.vala b/main/src/ui/conversation_content_view/file_image_widget.vala
index a3579185..d5415f31 100644
--- a/main/src/ui/conversation_content_view/file_image_widget.vala
+++ b/main/src/ui/conversation_content_view/file_image_widget.vala
@@ -1,51 +1,37 @@
using Gee;
using Gdk;
using Gtk;
+using Xmpp;
using Dino.Entities;
namespace Dino.Ui {
public class FileImageWidget : Box {
+ enum State {
+ EMPTY,
+ PREVIEW,
+ IMAGE
+ }
+ private State state = State.EMPTY;
- public FileImageWidget() {
- this.halign = Align.START;
+ private Stack stack = new Stack() { transition_duration=600, transition_type=StackTransitionType.CROSSFADE };
+ private Overlay overlay = new Overlay();
- this.add_css_class("file-image-widget");
- this.set_cursor_from_name("zoom-in");
- }
+ private bool show_image_overlay_toolbar = false;
+ private Gtk.Box image_overlay_toolbar = new Gtk.Box(Orientation.VERTICAL, 0) { halign=Align.END, valign=Align.START, margin_top=10, margin_start=10, margin_end=10, margin_bottom=10, vexpand=false, visible=false };
+ private Label file_size_label = new Label(null) { halign=Align.START, valign=Align.END, margin_bottom=4, margin_start=4, visible=false };
- public async void load_from_file(File file, string file_name, int MAX_WIDTH=600, int MAX_HEIGHT=300) throws GLib.Error {
- Gtk.Box image_overlay_toolbar = new Gtk.Box(Orientation.HORIZONTAL, 0) { halign=Gtk.Align.END, valign=Gtk.Align.START, margin_top=10, margin_start=10, margin_end=10, margin_bottom=10, vexpand=false, visible=false };
- image_overlay_toolbar.add_css_class("card");
- image_overlay_toolbar.add_css_class("toolbar");
- image_overlay_toolbar.add_css_class("overlay-toolbar");
- image_overlay_toolbar.set_cursor_from_name("default");
+ private FileTransfer file_transfer;
- FixedRatioPicture image = new FixedRatioPicture() { min_width=100, min_height=100, max_width=MAX_WIDTH, max_height=MAX_HEIGHT, file=file };
- GestureClick gesture_click_controller = new GestureClick();
- gesture_click_controller.button = 1; // listen for left clicks
- gesture_click_controller.released.connect((n_press, x, y) => {
- switch (gesture_click_controller.get_device().source) {
- case Gdk.InputSource.TOUCHSCREEN:
- case Gdk.InputSource.PEN:
- if (n_press == 1) {
- image_overlay_toolbar.visible = !image_overlay_toolbar.visible;
- } else if (n_press == 2) {
- this.activate_action("file.open", null);
- image_overlay_toolbar.visible = false;
- }
- break;
- default:
- this.activate_action("file.open", null);
- image_overlay_toolbar.visible = false;
- break;
- }
- });
- image.add_controller(gesture_click_controller);
+ private FileTransmissionProgress transmission_progress = new FileTransmissionProgress() { halign=Align.CENTER, valign=Align.CENTER, visible=false };
+
+ public FileImageWidget(int MAX_WIDTH=600, int MAX_HEIGHT=300) {
+ this.halign = Align.START;
- FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE);
+ this.add_css_class("file-image-widget");
+ // Setup menu button overlay
MenuButton button = new MenuButton();
button.icon_name = "view-more";
Menu menu_model = new Menu();
@@ -53,27 +39,216 @@ public class FileImageWidget : Box {
menu_model.append(_("Save as…"), "file.save_as");
Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu.from_model(menu_model);
button.popover = popover_menu;
-
image_overlay_toolbar.append(button);
+ image_overlay_toolbar.add_css_class("card");
+ image_overlay_toolbar.add_css_class("toolbar");
+ image_overlay_toolbar.add_css_class("overlay-toolbar");
+ image_overlay_toolbar.set_cursor_from_name("default");
- Overlay overlay = new Overlay();
- overlay.set_child(image);
+ file_size_label.add_css_class("file-details");
+
+ overlay.set_child(stack);
+ overlay.set_measure_overlay(stack, true);
+ overlay.add_overlay(file_size_label);
+ overlay.add_overlay(transmission_progress);
overlay.add_overlay(image_overlay_toolbar);
- overlay.set_measure_overlay(image, true);
overlay.set_clip_overlay(image_overlay_toolbar, true);
+ this.append(overlay);
+
+ GestureClick gesture_click_controller = new GestureClick();
+ gesture_click_controller.button = 1; // listen for left clicks
+ gesture_click_controller.released.connect(on_image_clicked);
+ stack.add_controller(gesture_click_controller);
+
EventControllerMotion this_motion_events = new EventControllerMotion();
this.add_controller(this_motion_events);
this_motion_events.enter.connect(() => {
- image_overlay_toolbar.visible = true;
+ image_overlay_toolbar.visible = show_image_overlay_toolbar;
+ file_size_label.visible = file_transfer != null && file_transfer.direction == FileTransfer.DIRECTION_RECEIVED && file_transfer.state == FileTransfer.State.NOT_STARTED && !file_transfer.sfs_sources.is_empty;
});
this_motion_events.leave.connect(() => {
if (button.popover != null && button.popover.visible) return;
image_overlay_toolbar.visible = false;
+ file_size_label.visible = false;
});
+ }
- this.append(overlay);
+ public async void set_file_transfer(FileTransfer file_transfer) {
+ this.file_transfer = file_transfer;
+
+ this.file_transfer.bind_property("size", file_size_label, "label", BindingFlags.SYNC_CREATE, (_, from_value, ref to_value) => {
+ to_value = FileDefaultWidget.get_size_string((int64) from_value);
+ return true;
+ });
+ this.file_transfer.bind_property("size", transmission_progress, "file-size", BindingFlags.SYNC_CREATE);
+ this.file_transfer.bind_property("transferred-bytes", transmission_progress, "transferred-size");
+
+ file_transfer.notify["state"].connect(refresh_state);
+ file_transfer.sources_changed.connect(refresh_state);
+ refresh_state();
+ }
+
+ private void refresh_state() {
+ if ((state == EMPTY || state == PREVIEW) && file_transfer.path != null) {
+ if (state == EMPTY) {
+ load_from_file.begin(file_transfer.get_file(), file_transfer.file_name);
+ show_image_overlay_toolbar = true;
+ } if (state == PREVIEW) {
+ Timeout.add(500, () => {
+ load_from_file.begin(file_transfer.get_file(), file_transfer.file_name);
+ show_image_overlay_toolbar = true;
+ return false;
+ });
+ }
+ this.set_cursor_from_name("zoom-in");
+
+ state = IMAGE;
+ } else if (state == EMPTY && file_transfer.thumbnails.size > 0) {
+ load_from_thumbnail.begin(file_transfer);
+
+ transmission_progress.visible = true;
+ show_image_overlay_toolbar = false;
+
+ state = PREVIEW;
+ }
+
+ if (file_transfer.state == IN_PROGRESS || file_transfer.state == NOT_STARTED || file_transfer.state == FAILED) {
+ transmission_progress.visible = true;
+ show_image_overlay_toolbar = false;
+ } else if (transmission_progress.visible) {
+ Timeout.add(500, () => {
+ transmission_progress.transferred_size = transmission_progress.file_size;
+ transmission_progress.visible = false;
+ show_image_overlay_toolbar = true;
+ return false;
+ });
+ }
+
+ if (file_transfer.state == FileTransfer.State.IN_PROGRESS) {
+ if (file_transfer.direction == FileTransfer.DIRECTION_RECEIVED) {
+ transmission_progress.state = FileTransmissionProgress.State.DOWNLOADING;
+ } else {
+ transmission_progress.state = FileTransmissionProgress.State.UPLOADING;
+ }
+ } else if (file_transfer.sfs_sources.is_empty) {
+ transmission_progress.state = UNKNOWN_SOURCE;
+ } else if (file_transfer.state == NOT_STARTED && file_transfer.direction == FileTransfer.DIRECTION_RECEIVED) {
+ transmission_progress.state = DOWNLOAD_NOT_STARTED;
+ } else if (file_transfer.state == FileTransfer.State.FAILED) {
+ transmission_progress.state = DOWNLOAD_NOT_STARTED_FAILED_BEFORE;
+ }
+ }
+
+ public async void load_from_file(File file, string file_name) throws GLib.Error {
+ FixedRatioPicture image = new FixedRatioPicture() { min_width=100, min_height=100, max_width=600, max_height=300 };
+ image.file = file;
+ stack.add_child(image);
+ stack.set_visible_child(image);
+ }
+
+ public async void load_from_thumbnail(FileTransfer file_transfer) throws GLib.Error {
+ this.file_transfer = file_transfer;
+
+ Gdk.Pixbuf? pixbuf = null;
+ foreach (Xep.JingleContentThumbnails.Thumbnail thumbnail in file_transfer.thumbnails) {
+ pixbuf = parse_thumbnail(thumbnail);
+ if (pixbuf != null) {
+ break;
+ }
+ }
+ if (pixbuf == null) {
+ warning("Can't load thumbnails of file %s", file_transfer.file_name);
+ throw new Error(-1, 0, "Error loading preview image");
+ }
+ // TODO: should this be executed? If yes, before or after scaling
+ pixbuf = pixbuf.apply_embedded_orientation();
+
+ if (file_transfer.width > 0 && file_transfer.height > 0) {
+ pixbuf = pixbuf.scale_simple(file_transfer.width, file_transfer.height, InterpType.BILINEAR);
+ } else {
+ warning("Preview: Not scaling image, width: %d, height: %d\n", file_transfer.width, file_transfer.height);
+ }
+ if (pixbuf == null) {
+ warning("Can't scale thumbnail %s", file_transfer.file_name);
+ throw new Error(-1, 0, "Error scaling preview image");
+ }
+
+ FixedRatioPicture image = new FixedRatioPicture() { min_width=100, min_height=100, max_width=600, max_height=300 };
+ image.paintable = Texture.for_pixbuf(pixbuf);
+ stack.add_child(image);
+ stack.set_visible_child(image);
+ }
+
+ public void on_image_clicked(GestureClick gesture_click_controller, int n_press, double x, double y) {
+ if (this.file_transfer.state != COMPLETE) return;
+
+ switch (gesture_click_controller.get_device().source) {
+ case Gdk.InputSource.TOUCHSCREEN:
+ case Gdk.InputSource.PEN:
+ if (n_press == 1) {
+ image_overlay_toolbar.visible = !image_overlay_toolbar.visible;
+ } else if (n_press == 2) {
+ this.activate_action("file.open", null);
+ image_overlay_toolbar.visible = false;
+ }
+ break;
+ default:
+ this.activate_action("file.open", null);
+ image_overlay_toolbar.visible = false;
+ break;
+ }
+ }
+
+ public static Pixbuf? parse_thumbnail(Xep.JingleContentThumbnails.Thumbnail thumbnail) {
+ string[] splits = thumbnail.uri.split(":", 2);
+ if (splits.length != 2) {
+ warning("Thumbnail parsing error: ':' not found");
+ return null;
+ }
+ if (splits[0] != "data") {
+ warning("Unsupported thumbnail: unimplemented uri type\n");
+ return null;
+ }
+ splits = splits[1].split(";", 2);
+ if (splits.length != 2) {
+ warning("Thumbnail parsing error: ';' not found");
+ return null;
+ }
+ if (splits[0] != "image/png") {
+ warning("Unsupported thumbnail: unsupported mime-type\n");
+ return null;
+ }
+ splits = splits[1].split(",", 2);
+ if (splits.length != 2) {
+ warning("Thumbnail parsing error: ',' not found");
+ return null;
+ }
+ if (splits[0] != "base64") {
+ warning("Unsupported thumbnail: data is not base64 encoded\n");
+ return null;
+ }
+ uint8[] data = Base64.decode(splits[1]);
+ MemoryInputStream input_stream = new MemoryInputStream.from_data(data);
+ Pixbuf pixbuf = new Pixbuf.from_stream(input_stream);
+ return pixbuf;
+ }
+
+ public static bool can_display(FileTransfer file_transfer) {
+ return file_transfer.mime_type != null && is_pixbuf_supported_mime_type(file_transfer.mime_type) &&
+ (file_transfer.state == FileTransfer.State.COMPLETE || file_transfer.thumbnails.size > 0);
+ }
+
+ public static bool is_pixbuf_supported_mime_type(string mime_type) {
+ if (mime_type == null) return false;
+
+ foreach (PixbufFormat pixbuf_format in Pixbuf.get_formats()) {
+ foreach (string pixbuf_mime in pixbuf_format.get_mime_types()) {
+ if (pixbuf_mime == mime_type) return true;
+ }
+ }
+ return false;
}
}