From 1c09eda7ff96dbe259d82c9cf7fc545874f1cbce Mon Sep 17 00:00:00 2001 From: bleakgrey Date: Wed, 9 May 2018 17:20:40 +0300 Subject: [PATCH] Implement media attachment to posts --- meson.build | 1 + src/Dialogs/PostDialog.vala | 62 ++++++++++++++++-------- src/NetManager.vala | 12 ++++- src/Widgets/AttachmentBox.vala | 70 +++++++++++++++++++++++++++ src/Widgets/AttachmentWidget.vala | 78 ++++++++++++++++++++++++++----- src/Widgets/StatusWidget.vala | 23 ++++----- 6 files changed, 200 insertions(+), 46 deletions(-) create mode 100644 src/Widgets/AttachmentBox.vala diff --git a/meson.build b/meson.build index c9d09c4..07acc72 100644 --- a/meson.build +++ b/meson.build @@ -41,6 +41,7 @@ executable( 'src/Widgets/StatusWidget.vala', 'src/Widgets/NotificationWidget.vala', 'src/Widgets/AttachmentWidget.vala', + 'src/Widgets/AttachmentBox.vala', 'src/Dialogs/PostDialog.vala', 'src/Views/AbstractView.vala', 'src/Views/AddAccountView.vala', diff --git a/src/Dialogs/PostDialog.vala b/src/Dialogs/PostDialog.vala index 60b9719..2ba53d9 100644 --- a/src/Dialogs/PostDialog.vala +++ b/src/Dialogs/PostDialog.vala @@ -8,7 +8,10 @@ public class Tootle.PostDialog : Gtk.Dialog { private Gtk.ScrolledWindow scroll; private Gtk.Label counter; private Gtk.MenuButton visibility; + private Gtk.Button attach; + private Gtk.Button cancel; private Gtk.Button publish; + private AttachmentBox attachments; private StatusVisibility visibility_opt; protected int64? in_reply_to_id; @@ -25,12 +28,24 @@ public class Tootle.PostDialog : Gtk.Dialog { var actions = get_action_area ().get_parent () as Gtk.Box; var content = get_content_area (); + get_action_area ().hexpand = false; visibility = get_visibility_btn (); - var close = add_button(_("Cancel"), 5) as Gtk.Button; - close.clicked.connect(() => { - this.destroy (); - }); + visibility.tooltip_text = _("Post Visibility"); + visibility.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + visibility.can_default = false; + visibility.set_focus_on_click (false); + attach = new Gtk.Button.from_icon_name ("mail-attachment-symbolic"); + attach.tooltip_text = _("Add Media"); + attach.valign = Gtk.Align.CENTER; + attach.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + attach.get_style_context ().remove_class ("image-button"); + attach.can_default = false; + attach.set_focus_on_click (false); + attach.clicked.connect (() => attachments.select ()); + + cancel = add_button(_("Cancel"), 5) as Gtk.Button; + cancel.clicked.connect(() => this.destroy ()); publish = add_button(_("Toot!"), 5) as Gtk.Button; publish.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); publish.clicked.connect (() => { @@ -50,17 +65,23 @@ public class Tootle.PostDialog : Gtk.Dialog { scroll.add (text); scroll.show_all (); + attachments = new AttachmentBox (true); counter = new Gtk.Label ("500"); - actions.pack_start (visibility, false, false, 6); actions.pack_start (counter, false, false, 6); - content.pack_start (scroll, false, false, 0); + actions.pack_end (visibility, false, false, 0); + actions.pack_end (attach, false, false, 6); + content.pack_start (scroll, false, false, 6); + content.pack_start (attachments, false, false, 6); content.set_size_request (350, 150); + + show_all (); + attachments.hide (); } - private Gtk.MenuButton get_visibility_btn (){ + private Gtk.MenuButton get_visibility_btn () { var button = new Gtk.MenuButton (); - var icon = new Gtk.Image.from_icon_name (visibility_opt.get_icon (), Gtk.IconSize.SMALL_TOOLBAR); + var icon = new Gtk.Image.from_icon_name (visibility_opt.get_icon (), Gtk.IconSize.BUTTON); var menu = new Gtk.Popover (null); var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); box.margin = 6; @@ -95,7 +116,7 @@ public class Tootle.PostDialog : Gtk.Dialog { return button; } - private void update_counter (){ + private void update_counter () { var len = text.buffer.text.length; var remain = 500 - len; publish.sensitive = (remain >= 0); @@ -103,13 +124,12 @@ public class Tootle.PostDialog : Gtk.Dialog { counter.label = remain.to_string (); } - public static void open (Gtk.Window? parent, string? text = null){ + public static void open (Gtk.Window? parent, string? text = null) { if(dialog == null){ dialog = new PostDialog (parent); dialog.destroy.connect (() => { dialog = null; }); - dialog.show_all (); if (text != null) dialog.text.buffer.text = text; } @@ -117,7 +137,7 @@ public class Tootle.PostDialog : Gtk.Dialog { dialog.text.buffer.text += " " + text; } - public static void open_reply (Gtk.Window? parent, Status status){ + public static void open_reply (Gtk.Window? parent, Status status) { if(dialog == null){ open (parent); dialog.in_reply_to_id = status.id; @@ -125,16 +145,18 @@ public class Tootle.PostDialog : Gtk.Dialog { } } - public void publish_post(){ - var text_escaped = text.buffer.text.replace (" ", "%20"); - var pars = "?status=" + text_escaped; + public void publish_post () { + var pars = "?status=%s".printf (Soup.URI.encode (text.buffer.text, null)); + pars += "&visibility=%s".printf (visibility_opt.to_string ()); + pars += attachments.get_uri_array (); if (in_reply_to_id != null) - pars += "&in_reply_to_id=" + in_reply_to_id.to_string (); - pars += "&visibility=" + visibility_opt.to_string (); - - var msg = new Soup.Message("POST", Tootle.settings.instance_url + "/api/v1/statuses" + pars); + pars += "&in_reply_to_id=%s".printf (in_reply_to_id.to_string ()); + + var url = "%s/api/v1/statuses%s".printf (Tootle.settings.instance_url, pars); + var msg = new Soup.Message("POST", url); + debug (url); Tootle.network.queue(msg, (sess, mess) => { - try{ + try { var root = Tootle.network.parse (mess); var status = Status.parse (root); debug (status.id.to_string ()); //TODO: Live updates diff --git a/src/NetManager.vala b/src/NetManager.vala index 50a25d5..b55d7b0 100644 --- a/src/NetManager.vala +++ b/src/NetManager.vala @@ -30,7 +30,7 @@ public class Tootle.NetManager : GLib.Object { finished (); }); - // Soup.Logger logger = new Soup.Logger (Soup.LoggerLogLevel.MINIMAL, -1); + // Soup.Logger logger = new Soup.Logger (Soup.LoggerLogLevel.BODY, -1); // session.add_feature (logger); } @@ -114,5 +114,15 @@ public class Tootle.NetManager : GLib.Object { }); Tootle.network.queue (msg); } + + public void load_scaled_image (string url, Gtk.Image image, int size = 64) { + var msg = new Soup.Message("GET", url); + msg.finished.connect(() => { + var stream = new MemoryInputStream.from_data(msg.response_body.data, GLib.g_free); + var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true); + image.set_from_pixbuf (pixbuf); + }); + Tootle.network.queue (msg); + } } diff --git a/src/Widgets/AttachmentBox.vala b/src/Widgets/AttachmentBox.vala new file mode 100644 index 0000000..0c4f12d --- /dev/null +++ b/src/Widgets/AttachmentBox.vala @@ -0,0 +1,70 @@ +using Gtk; +using GLib; + +public class Tootle.AttachmentBox : Gtk.ScrolledWindow { + + private Gtk.Box box; + private bool edit_mode; + private int64[] ids; + + construct { + box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + box.hexpand = true; + add (box); + show_all (); + } + + public AttachmentBox (bool edit = false) { + Object (); + edit_mode = edit; + vscrollbar_policy = Gtk.PolicyType.NEVER; + } + + public void clear () { + box.forall (widget => widget.destroy ()); + } + + public void append (Attachment attachment) { + var widget = new AttachmentWidget (attachment); + box.add (widget); + } + + public void select () { + var filter = new Gtk.FileFilter (); + filter.add_mime_type ("image/jpeg"); + filter.add_mime_type ("image/png"); + filter.add_mime_type ("image/gif"); + filter.add_mime_type ("video/webm"); + filter.add_mime_type ("video/mp4"); + + var chooser = new Gtk.FileChooserDialog ( + _("Select media files to add"), + null, + Gtk.FileChooserAction.OPEN, + _("_Cancel"), + Gtk.ResponseType.CANCEL, + _("_Open"), + Gtk.ResponseType.ACCEPT); + + chooser.select_multiple = true; + chooser.set_filter (filter); + + if (chooser.run () == Gtk.ResponseType.ACCEPT) { + show (); + foreach (unowned string uri in chooser.get_uris ()) { + var widget = new AttachmentWidget.upload (uri); + widget.uploaded.connect (id => ids += id); + box.pack_start (widget, false, false, 6); + } + } + chooser.close (); + } + + public string get_uri_array () { + var str = ""; + foreach (int64 item in ids) + str += "&media_ids[]=" + item.to_string (); + return str; + } + +} diff --git a/src/Widgets/AttachmentWidget.vala b/src/Widgets/AttachmentWidget.vala index f499afe..b952db4 100644 --- a/src/Widgets/AttachmentWidget.vala +++ b/src/Widgets/AttachmentWidget.vala @@ -2,43 +2,99 @@ using Gtk; public class Tootle.AttachmentWidget : Gtk.EventBox { - Attachment attachment; + public abstract signal void uploaded (int64 id); + public abstract signal void removed (int64 id); + + Attachment? attachment; + private bool editable = false; + + public Gtk.Label label; Gtk.Grid grid; - Gtk.Label? label; Gtk.Image? image; construct { - margin_top = 6; + set_size_request (64, 64); grid = new Gtk.Grid (); get_style_context ().add_class ("attachment"); + + label = new Gtk.Label (""); + label.wrap = true; + label.vexpand = true; + label.margin_start = label.margin_end = 8; + grid.attach (label, 0, 0); + add (grid); + grid.show (); + label.hide (); } public AttachmentWidget (Attachment att) { attachment = att; + rebind (); + } + + public void rebind () { var type = attachment.type; - switch (type){ case "image": image = new Gtk.Image (); image.vexpand = true; image.margin = 3; image.valign = Gtk.Align.CENTER; - Tootle.network.load_image (attachment.preview_url, image); + image.show (); + if (editable) + Tootle.network.load_scaled_image (attachment.preview_url, image); + else + Tootle.network.load_image (attachment.preview_url, image); grid.attach (image, 0, 0); + label.hide (); break; default: - label = new Gtk.Label (_("Click to open %s media").printf (type)); - label.margin = 16; - grid.attach (label, 0, 0); + label.label = _("Click to open %s media").printf (type); + label.show (); break; } - + show (); button_press_event.connect(() => { - Tootle.Utils.open_url (attachment.url); + if (!editable) + Tootle.Utils.open_url (attachment.url); return true; }); - show_all (); + } + + public AttachmentWidget.upload (string uri) { + try { + GLib.File file = File.new_for_uri (uri); + uint8[] contents; + file.load_contents (null, out contents, null); + var type = file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, 0); + var mime = type.get_content_type (); + + debug ("Uploading %s (%s)", uri, mime); + label.label = _("Uploading file..."); + label.show (); + show (); + + var buffer = new Soup.Buffer.take (contents); + var multipart = new Soup.Multipart (Soup.FORM_MIME_TYPE_MULTIPART); + multipart.append_form_file ("file", mime.replace ("/", "."), mime, buffer); + var url = "%s/api/v1/media".printf (Tootle.settings.instance_url); + var msg = Soup.Form.request_new_from_multipart (url, multipart); + + Tootle.network.queue(msg, (sess, mess) => { + var root = Tootle.network.parse (mess); + attachment = Attachment.parse (root); + editable = true; + rebind (); + + debug ("Uploaded media: %lld", attachment.id); + uploaded (attachment.id); + }); + } + catch (Error e) { + error (e.message); + Tootle.app.error (_("File read error"), _("Can't read file %s: %s").printf (uri, e.message)); + } } } diff --git a/src/Widgets/StatusWidget.vala b/src/Widgets/StatusWidget.vala index 2f032bc..e927307 100644 --- a/src/Widgets/StatusWidget.vala +++ b/src/Widgets/StatusWidget.vala @@ -16,8 +16,7 @@ public class Tootle.StatusWidget : Gtk.EventBox { public Tootle.RichLabel content_label; public Tootle.RichLabel? content_spoiler; Gtk.Box title_box; - Gtk.Box attachments; - Gtk.ScrolledWindow attachments_scroll; + AttachmentBox attachments; Gtk.Grid grid; Gtk.Box counters; Gtk.Label reblogs; @@ -59,6 +58,8 @@ public class Tootle.StatusWidget : Gtk.EventBox { content_label = new RichLabel (""); content_label.wrap_words (); + attachments = new AttachmentBox (); + reblogs = new Gtk.Label ("0"); favorites = new Gtk.Label ("0"); @@ -80,22 +81,16 @@ public class Tootle.StatusWidget : Gtk.EventBox { reply.set_active (false); PostDialog.open_reply (Tootle.window, this.status); }); - - attachments = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); - attachments.hexpand = true; - attachments_scroll = new ScrolledWindow (null, null); - attachments_scroll.vscrollbar_policy = Gtk.PolicyType.NEVER; - attachments_scroll.add (attachments); var revealer_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); revealer_box.margin_end = 12; revealer_box.add (content_label); - revealer_box.add (attachments_scroll); + revealer_box.add (attachments); revealer = new Revealer (); revealer.reveal_child = true; revealer.add (revealer_box); - counters = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); //TODO: currently useless + counters = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); counters.margin_top = 6; counters.margin_bottom = 6; counters.add (reblog); @@ -111,8 +106,6 @@ public class Tootle.StatusWidget : Gtk.EventBox { grid.attach (counters, 2, 5, 1, 1); add (grid); show_all (); - - attachments_scroll.hide (); } public StatusWidget (Status status) { @@ -171,10 +164,12 @@ public class Tootle.StatusWidget : Gtk.EventBox { } if (status.attachments != null) { - attachments_scroll.show (); + attachments.clear (); foreach (Attachment attachment in status.attachments) - attachments.add (new AttachmentWidget (attachment)); + attachments.append (attachment); } + else + attachments.destroy (); destroy.connect (() => { if(separator != null)