diff options
| author | fiaxh <git@lightrise.org> | 2026-08-14 11:45:08 +0200 |
|---|---|---|
| committer | fiaxh <git@lightrise.org> | 2026-08-21 18:18:03 +0200 |
| commit | 8d49d83b3b45ab22d7b9d945c4b32296b07cb49e (patch) | |
| tree | 5a0bf3d777c4efd806388163fc9dc74c87edd07b /main/src/ui/conversation_content_view | |
| parent | 6d0706bcaa6d54354e416174e3d2654a2bff81fe (diff) | |
Add initial support for multi-file transfers
Diffstat (limited to 'main/src/ui/conversation_content_view')
6 files changed, 229 insertions, 115 deletions
diff --git a/main/src/ui/conversation_content_view/content_populator.vala b/main/src/ui/conversation_content_view/content_populator.vala index ef859bde..4d1b8c90 100644 --- a/main/src/ui/conversation_content_view/content_populator.vala +++ b/main/src/ui/conversation_content_view/content_populator.vala @@ -70,6 +70,8 @@ public class ContentProvider : ContentItemCollection, Object { return new FileMetaItem(content_item, stream_interactor); } else if (content_item.type_ == CallItem.TYPE) { return new CallMetaItem(content_item, stream_interactor); + } else if (content_item.type_ == FileGroupItem.TYPE) { + return new FileGroupMetaItem(content_item, stream_interactor); } critical("Got unknown content item type %s", content_item.type_); return null; diff --git a/main/src/ui/conversation_content_view/file_default_widget.vala b/main/src/ui/conversation_content_view/file_default_widget.vala index a21e2afb..1ad3d6c2 100644 --- a/main/src/ui/conversation_content_view/file_default_widget.vala +++ b/main/src/ui/conversation_content_view/file_default_widget.vala @@ -51,13 +51,13 @@ public class FileDefaultWidget : Box { mime_label.label = content_type != null ? content_type.get_description() : null; } - public void update_file_info(Xmpp.FileContentType? content_type, FileTransfer.State state, bool direction, int64 size, int64 transferred_bytes) { - this.state = state; + public void update_file_info(FileTransfer file_transfer) { + this.state = file_transfer.state; spinner.stop(); // A hidden spinning spinner still uses CPU. Deactivate asap - content_type_image.icon_name = get_file_icon_name(content_type); - string? mime_description = content_type != null ? content_type.get_description() : null; + content_type_image.icon_name = get_file_icon_name(file_transfer.content_type); + string? mime_description = file_transfer.content_type != null ? file_transfer.content_type.get_description() : null; switch (state) { case FileTransfer.State.COMPLETE: @@ -66,23 +66,31 @@ public class FileDefaultWidget : Box { // Create a menu Menu menu_model = new Menu(); - menu_model.append(_("Open"), "file.open"); - menu_model.append(_("Save as…"), "file.save_as"); + + MenuItem open_file_item = new MenuItem(_("Open"), "app.file_open_externally"); + open_file_item.set_action_and_target_value("app.file_open_externally", new Variant.int32(file_transfer.id)); + + MenuItem save_as_item = new MenuItem(_("Save as…"), "app.file_save_as"); + save_as_item.set_action_and_target_value("app.file_open_save_dialog", new Variant.int32(file_transfer.id)); + + menu_model.append_item(open_file_item); + menu_model.append_item(save_as_item); + Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu.from_model(menu_model); file_menu.popover = popover_menu; popover_menu.closed.connect(on_pointer_left); break; case FileTransfer.State.IN_PROGRESS: - if (direction == FileTransfer.DIRECTION_RECEIVED) { - if (size > 0) { - int64 progress = transferred_bytes * 100 / size; - mime_label.label = _("Downloading %s… (%u%%)").printf(get_size_string(size), progress); + if (file_transfer.direction == FileTransfer.DIRECTION_RECEIVED) { + if (file_transfer.size > 0) { + int64 progress = file_transfer.transferred_bytes * 100 / file_transfer.size; + mime_label.label = _("Downloading %s… (%u%%)").printf(get_size_string(file_transfer.size), progress); } else { - mime_label.label = _("Downloading %s…").printf(get_size_string(size)); + mime_label.label = _("Downloading %s…").printf(get_size_string(file_transfer.size)); } } else { - int64 progress = transferred_bytes * 100 / size; - mime_label.label = _("Uploading %s… (%u%%)").printf(get_size_string(size), progress); + int64 progress = file_transfer.transferred_bytes * 100 / file_transfer.size; + mime_label.label = _("Uploading %s… (%u%%)").printf(get_size_string(file_transfer.size), progress); } spinner.start(); image_stack.set_visible_child_name("spinner"); @@ -96,9 +104,9 @@ public class FileDefaultWidget : Box { break; case FileTransfer.State.NOT_STARTED: if (mime_description != null) { - mime_label.label = _("%s offered: %s").printf(mime_description, get_size_string(size)); - } else if (size != -1) { - mime_label.label = _("File offered: %s").printf(get_size_string(size)); + mime_label.label = _("%s offered: %s").printf(mime_description, get_size_string(file_transfer.size)); + } else if (file_transfer.size != -1) { + mime_label.label = _("File offered: %s").printf(get_size_string(file_transfer.size)); } else { mime_label.label = _("File offered"); } diff --git a/main/src/ui/conversation_content_view/file_group_widget.vala b/main/src/ui/conversation_content_view/file_group_widget.vala new file mode 100644 index 00000000..80c41a7a --- /dev/null +++ b/main/src/ui/conversation_content_view/file_group_widget.vala @@ -0,0 +1,120 @@ +using Gee; +using Gdk; +using Gtk; +using Pango; +using Xmpp; + +using Dino.Entities; + +namespace Dino.Ui { + + public class FileGroupMetaItem : ConversationSummary.ContentMetaItem { + + private StreamInteractor stream_interactor; + private FileGroupItem file_group_item; + + public FileGroupMetaItem(ContentItem content_item, StreamInteractor stream_interactor) { + base(content_item); + this.stream_interactor = stream_interactor; + this.file_group_item = content_item as FileGroupItem; + } + + public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { + var file_transfers = file_group_item.file_group.file_transfers; + + if (file_transfers.size > 1 && all_files_images(file_transfers)) { + return new ImageGroupWidget(file_transfers); + } else { + Box box = new Box(Orientation.VERTICAL, 6); + foreach (FileTransfer file_transfer in file_transfers) { + FileWidget widget = new FileWidget(file_transfer) { halign = Align.START }; + widget.add_css_class("file-image-widget"); + FileWidgetController widget_controller = new FileWidgetController(widget, file_transfer, stream_interactor); + box.append(widget); + } + return box; + } + } + + public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) { + Gee.List<Plugins.MessageAction> actions = new ArrayList<Plugins.MessageAction>(); + return actions; + } + } + + public class ImageGroupWidget : Box { + + private Gee.ArrayList<FileTransfer> file_transfers; + + public ImageGroupWidget(Gee.ArrayList<FileTransfer> file_transfers) { + this.file_transfers = file_transfers; + + if (file_transfers.size == 3) { + Box box = new Box(Orientation.HORIZONTAL, 4) { + overflow = Overflow.HIDDEN, + halign = Align.START + }; + box.add_css_class("image-group"); + + var picture1 = new FileImageWidget.from_file_transfer(file_transfers[0], 200); + box.append(picture1); + + Box box1 = new Box(Orientation.VERTICAL, 4); + + var picture2 = new FileImageWidget.from_file_transfer(file_transfers[1], 100); + box1.append(picture2); + var picture3 = new FileImageWidget.from_file_transfer(file_transfers[2], 100); + box1.append(picture3); + box.append(box1); + + this.append(box); + } else { + Box box = new Box(Orientation.VERTICAL, 4) { + overflow = Overflow.HIDDEN, + halign = Align.START + }; + box.add_css_class("image-group"); + + Box row_upper_row = new Box(Orientation.HORIZONTAL, 4); + + var picture1 = new FileImageWidget.from_file_transfer(file_transfers[0], 150); + row_upper_row.append(picture1); + + var picture2 = new FileImageWidget.from_file_transfer(file_transfers[1], 150); + row_upper_row.append(picture2); + box.append(row_upper_row); + + if (file_transfers.size >= 4) { + + int size = 150; + if (file_transfers.size == 5) size = 99; + + Box row_lower_row = new Box(Orientation.HORIZONTAL, 4); + + var picture3 = new FileImageWidget.from_file_transfer(file_transfers[2], size); + row_lower_row.append(picture3); + + var picture4 = new FileImageWidget.from_file_transfer(file_transfers[3], size); + row_lower_row.append(picture4); + + if (file_transfers.size == 5) { + var picture5 = new FileImageWidget.from_file_transfer(file_transfers[4], size); + row_lower_row.append(picture5); + } + + box.append(row_lower_row); + } + this.append(box); + } + } + } + + public static bool all_files_images(Gee.ArrayList<FileTransfer> file_transfers) { + foreach (FileTransfer file_transfer in file_transfers) { + if (!Dino.Util.is_pixbuf_supported_content_type(file_transfer.content_type)) { + return false; + } + } + return true; + } +}
\ No newline at end of file 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 e936ca25..9f12f892 100644 --- a/main/src/ui/conversation_content_view/file_image_widget.vala +++ b/main/src/ui/conversation_content_view/file_image_widget.vala @@ -22,6 +22,8 @@ public class FileImageWidget : Widget { 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 }; + private int size = -1; + private FileTransfer file_transfer; private FileTransmissionProgress transmission_progress = new FileTransmissionProgress() { halign=Align.CENTER, valign=Align.CENTER, visible=false }; @@ -34,22 +36,6 @@ public class FileImageWidget : Widget { public FileImageWidget(int MAX_WIDTH=600, int MAX_HEIGHT=300) { this.halign = Align.START; - this.add_css_class("file-image-widget"); - - // Setup menu button overlay - MenuButton button = new MenuButton(); - button.icon_name = "dino-view-more"; - Menu menu_model = new Menu(); - menu_model.append(_("Open"), "file.open"); - 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("compact-card-toolbar"); - image_overlay_toolbar.set_cursor_from_name("default"); - file_size_label.add_css_class("file-details"); overlay.set_child(stack); @@ -65,12 +51,65 @@ public class FileImageWidget : Widget { gesture_click_controller.button = 1; // listen for left clicks gesture_click_controller.released.connect(on_image_clicked); stack.add_controller(gesture_click_controller); + } + + public FileImageWidget.from_file_transfer(FileTransfer file_transfer, int size = -1) { + this(); + + this.file_transfer = file_transfer; + this.size = size; + + this.file_transfer.bind_property("size", file_size_label, "label", BindingFlags.SYNC_CREATE, file_size_label_transform); + this.file_transfer.bind_property("size", transmission_progress, "file-size", BindingFlags.SYNC_CREATE); + this.file_transfer.bind_property("transferred-bytes", transmission_progress, "transferred-size"); + + setup_signals(transmission_progress, file_transfer); + setup_menu_button(); + + file_transfer.notify["state"].connect(refresh_state); + file_transfer.sources_changed.connect(refresh_state); + refresh_state(); + } + + private static void setup_signals(FileTransmissionProgress transmission_progress, FileTransfer file_transfer) { + var file_transfer_id = file_transfer.id; + + transmission_progress.start_download.connect(() => { + Dino.Application.get_default().activate_action("file_start_download", new Variant.int32(file_transfer_id)); + }); + transmission_progress.cancel_transfer.connect(() => { + Dino.Application.get_default().activate_action("file_cancel_transfer", new Variant.int32(file_transfer_id)); + }); + } + + private void setup_menu_button() { + MenuButton button = new MenuButton(); + button.icon_name = "dino-view-more"; + Menu menu_model = new Menu(); + + MenuItem open_file_item = new MenuItem(_("Open"), "app.file_open_externally"); + open_file_item.set_action_and_target_value("app.file_open_externally", new Variant.int32(file_transfer.id)); + + MenuItem save_as_item = new MenuItem(_("Save as…"), "app.file_save_as"); + save_as_item.set_action_and_target_value("app.file_open_save_dialog", new Variant.int32(file_transfer.id)); + + menu_model.append_item(open_file_item); + menu_model.append_item(save_as_item); + + 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("compact-card-toolbar"); + image_overlay_toolbar.set_cursor_from_name("default"); EventControllerMotion this_motion_events = new EventControllerMotion(); this.add_controller(this_motion_events); this_motion_events.enter.connect((controller, x, y) => { (controller.widget as FileImageWidget).on_motion_event_enter(); }); + attach_on_motion_event_leave(this_motion_events, button); } @@ -88,18 +127,6 @@ public class FileImageWidget : Widget { 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; } - 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, file_size_label_transform); - 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 static bool file_size_label_transform(Binding binding, Value from_value, ref Value to_value) { to_value = FileDefaultWidget.get_size_string((int64) from_value); return true; @@ -153,7 +180,7 @@ public class FileImageWidget : Widget { } 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 }; + FixedRatioPicture image = create_picture(); FileInputStream file_stream = null; try { @@ -201,7 +228,7 @@ public class FileImageWidget : Widget { 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 }; + FixedRatioPicture image = create_picture(); image.paintable = Texture.for_pixbuf(pixbuf); stack.add_child(image); stack.set_visible_child(image); @@ -216,12 +243,12 @@ public class FileImageWidget : Widget { if (n_press == 1) { image_overlay_toolbar.visible = !image_overlay_toolbar.visible; } else if (n_press == 2) { - this.activate_action("file.open", null); + Dino.Application.get_default().activate_action("file_open_externally", new Variant.int32(file_transfer.id)); image_overlay_toolbar.visible = false; } break; default: - this.activate_action("file.open", null); + Dino.Application.get_default().activate_action("file_open_externally", new Variant.int32(file_transfer.id)); image_overlay_toolbar.visible = false; break; } @@ -237,6 +264,20 @@ public class FileImageWidget : Widget { (file_transfer.state == FileTransfer.State.COMPLETE || file_transfer.thumbnails.size > 0); } + private FixedRatioPicture create_picture() { + if (size == -1) { + return new FixedRatioPicture() { + min_width=100, max_width=600, + min_height=100, max_height=300 + }; + } else { + return new FixedRatioPicture() { + height_request = size, width_request = size, + content_fit = ContentFit.COVER, halign = Align.START + }; + } + } + public override void dispose() { if (overlay != null && overlay.parent != null) overlay.unparent(); base.dispose(); diff --git a/main/src/ui/conversation_content_view/file_transmission_progress.vala b/main/src/ui/conversation_content_view/file_transmission_progress.vala index e3f33448..43885c1b 100644 --- a/main/src/ui/conversation_content_view/file_transmission_progress.vala +++ b/main/src/ui/conversation_content_view/file_transmission_progress.vala @@ -9,6 +9,9 @@ namespace Dino.Ui { public class FileTransmissionProgress : Adw.Bin { + public signal void start_download(); + public signal void cancel_transfer(); + public enum State { UNKNOWN_SOURCE, DOWNLOAD_NOT_STARTED, @@ -124,11 +127,12 @@ namespace Dino.Ui { break; case DOWNLOAD_NOT_STARTED_FAILED_BEFORE: case DOWNLOAD_NOT_STARTED: + start_download(); this.activate_action("file.download", null); break; case DOWNLOADING: case UPLOADING: - this.activate_action("file.cancel", null); + cancel_transfer(); break; } } diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala index ecc2d3e9..04185343 100644 --- a/main/src/ui/conversation_content_view/file_widget.vala +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -57,18 +57,6 @@ public class FileWidget : SizeRequestBin { private FileDefaultWidgetController default_widget_controller; private Widget? content = null; - public signal void open_file(); - public signal void save_file_as(); - public signal void start_download(); - public signal void cancel_download(); - - class construct { - install_action("file.open", null, (widget, action_name) => { ((FileWidget) widget).open_file(); }); - install_action("file.save_as", null, (widget, action_name) => { ((FileWidget) widget).save_file_as(); }); - install_action("file.download", null, (widget, action_name) => { ((FileWidget) widget).start_download(); }); - install_action("file.cancel", null, (widget, action_name) => { ((FileWidget) widget).cancel_download(); }); - } - construct { margin_top = 4; size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH; @@ -79,11 +67,6 @@ public class FileWidget : SizeRequestBin { this.file_transfer = file_transfer; update_widget.begin(); -// size_allocate.connect((allocation) => { -// if (allocation.height > parent.get_allocated_height()) { -// Idle.add(() => { parent.queue_resize(); return false; }); -// } -// }); file_transfer.bind_property("state", this, "file-transfer-state"); file_transfer.bind_property("content-type", this, "file-transfer-content-type"); @@ -100,8 +83,8 @@ public class FileWidget : SizeRequestBin { FileImageWidget file_image_widget = null; try { - file_image_widget = new FileImageWidget(); - yield file_image_widget.set_file_transfer(file_transfer); + file_image_widget = new FileImageWidget.from_file_transfer(file_transfer); + file_image_widget.add_css_class("file-image-widget"); // If the widget changed in the meanwhile, stop if (content != content_bak) return; @@ -152,50 +135,6 @@ public class FileWidgetController : Object { }); this.file_transfer = file_transfer; this.stream_interactor = stream_interactor; - - widget.open_file.connect(open_file); - widget.save_file_as.connect(save_file); - widget.start_download.connect(start_download); - widget.cancel_download.connect(cancel_download); - } - - private void open_file() { - try{ - AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); - } catch (Error err) { - warning("Failed to open %s - %s", file_transfer.get_file().get_uri(), err.message); - } - } - - private void save_file() { - var save_dialog = new FileChooserNative(_("Save as…"), widget.get_root() as Gtk.Window, FileChooserAction.SAVE, null, null); - save_dialog.set_modal(true); - save_dialog.set_current_name(file_transfer.file_name); - - save_dialog.response.connect(() => { - File? target_file = save_dialog.get_file(); - if (target_file == null) { - warning("No file returned from save dialog."); - return; - } - try { - file_transfer.get_file().copy(save_dialog.get_file(), GLib.FileCopyFlags.OVERWRITE, null); - } catch (Error err) { - warning("Failed copy file %s - %s", file_transfer.get_file().get_uri(), err.message); - } - }); - - save_dialog.show(); - } - - private void start_download() { - if (stream_interactor != null) { - stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer); - } - } - - private void cancel_download() { - file_transfer.cancellable.cancel(); } } @@ -234,16 +173,16 @@ public class FileDefaultWidgetController : Object { private void update_file_info() { state = file_transfer.state; - widget.update_file_info(file_transfer.content_type, file_transfer.state, file_transfer.direction, file_transfer.size, file_transfer.transferred_bytes); + widget.update_file_info(file_transfer); } private void on_clicked() { switch (state) { case FileTransfer.State.COMPLETE: - widget.activate_action("file.open", null); + Dino.Application.get_default().activate_action("file_open_externally", new GLib.Variant.int32(file_transfer.id)); break; case FileTransfer.State.NOT_STARTED: - widget.activate_action("file.download", null); + Dino.Application.get_default().activate_action("file_start_download", new GLib.Variant.int32(file_transfer.id)); break; default: // Clicking doesn't do anything in FAILED and IN_PROGRESS states |
