feat(attachmentspage): uploading & re-design (#66)
* feat(AttachmentsPage): use instance info supported mimes * feat(AttachmentsPage): redesign * feat(AttachmentsPage): AttachmentsPageAttachment * feat: media upload + publish * chore: push AttachmentsPageAttachment * feat: toast overlay * feat: attachment size check
This commit is contained in:
parent
d3d3596210
commit
72388cf56e
|
@ -182,6 +182,10 @@ headerbar.flat.no-title .title {
|
|||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.ttl-status-badge > .image-button {
|
||||
padding: 3px 12px;
|
||||
}
|
||||
|
||||
.ttl-verified-field {
|
||||
color: rgb(27,133,83);
|
||||
background: rgba(27,133,83,0.1);
|
||||
|
|
|
@ -81,6 +81,7 @@ sources = files(
|
|||
'src/API/PollOption.vala',
|
||||
'src/Application.vala',
|
||||
'src/Dialogs/Composer/AttachmentsPage.vala',
|
||||
'src/Dialogs/Composer/AttachmentsPageAttachment.vala',
|
||||
'src/Dialogs/Composer/Dialog.vala',
|
||||
'src/Dialogs/Composer/EditorPage.vala',
|
||||
'src/Dialogs/Composer/Page.vala',
|
||||
|
|
|
@ -18,65 +18,57 @@ public class Tooth.API.Attachment : Entity, Widgetizable {
|
|||
}
|
||||
}
|
||||
|
||||
public static Attachment upload (File file) {
|
||||
return new Attachment () {
|
||||
source_file = file
|
||||
};
|
||||
// public static Attachment upload (File file) {
|
||||
// return new Attachment () {
|
||||
// source_file = file
|
||||
// };
|
||||
// }
|
||||
|
||||
public static async Attachment upload (string uri) throws Error {
|
||||
message (@"Uploading new media: $(uri)...");
|
||||
|
||||
uint8[] contents;
|
||||
string mime;
|
||||
GLib.FileInfo type;
|
||||
try {
|
||||
GLib.File file = File.new_for_uri (uri);
|
||||
file.load_contents (null, out contents, null);
|
||||
type = file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, 0);
|
||||
mime = type.get_content_type ();
|
||||
}
|
||||
catch (Error e) {
|
||||
throw new Oopsie.USER (_("Can't open file %s:\n%s").printf (uri, e.message));
|
||||
}
|
||||
|
||||
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 = @"$(accounts.active.instance)/api/v1/media";
|
||||
var msg = Soup.Form.request_new_from_multipart (url, multipart);
|
||||
msg.request_headers.append ("Authorization", @"Bearer $(accounts.active.access_token)");
|
||||
|
||||
string? error = null;
|
||||
network.queue (msg,
|
||||
(sess, mess) => {
|
||||
upload.callback ();
|
||||
},
|
||||
(code, reason) => {
|
||||
error = reason;
|
||||
upload.callback ();
|
||||
});
|
||||
|
||||
yield;
|
||||
|
||||
if (error != null)
|
||||
throw new Oopsie.INSTANCE (error);
|
||||
else {
|
||||
var node = network.parse_node (msg);
|
||||
var entity = accounts.active.create_entity<API.Attachment> (node);
|
||||
message (@"OK! ID $(entity.id)");
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
// public static async Attachment upload (string uri, string title, string? descr) throws Error {
|
||||
// message (@"Uploading new media: $(uri)...");
|
||||
|
||||
// uint8[] contents;
|
||||
// string mime;
|
||||
// GLib.FileInfo type;
|
||||
// try {
|
||||
// GLib.File file = File.new_for_uri (uri);
|
||||
// file.load_contents (null, out contents, null);
|
||||
// type = file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, 0);
|
||||
// mime = type.get_content_type ();
|
||||
// }
|
||||
// catch (Error e) {
|
||||
// throw new Oopsie.USER (_("Can't open file $file:\n$reason")
|
||||
// .replace ("$file", title)
|
||||
// .replace ("$reason", e.message)
|
||||
// );
|
||||
// }
|
||||
|
||||
// var descr_param = "";
|
||||
// if (descr != null && descr.replace (" ", "") != "") {
|
||||
// descr_param = "?description=" + HtmlUtils.uri_encode (descr);
|
||||
// }
|
||||
|
||||
// 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 = @"$(accounts.active.instance)/api/v1/media$descr_param";
|
||||
// var msg = Soup.Form.request_new_from_multipart (url, multipart);
|
||||
// msg.request_headers.append ("Authorization", @"Bearer $(accounts.active.access_token)");
|
||||
|
||||
// string? error = null;
|
||||
// network.queue (msg,
|
||||
// (sess, mess) => {
|
||||
// upload.callback ();
|
||||
// },
|
||||
// (code, reason) => {
|
||||
// error = reason;
|
||||
// upload.callback ();
|
||||
// });
|
||||
|
||||
// yield;
|
||||
|
||||
// if (error != null)
|
||||
// throw new Oopsie.INSTANCE (error);
|
||||
// else {
|
||||
// var node = network.parse_node (msg);
|
||||
// var entity = accounts.active.create_entity<API.Attachment> (node);
|
||||
// message (@"OK! ID $(entity.id)");
|
||||
// return entity;
|
||||
// }
|
||||
// }
|
||||
|
||||
public override Gtk.Widget to_widget () {
|
||||
if (preview_url != null) {
|
||||
return new Widgets.Attachment.Image () {
|
||||
|
|
|
@ -30,6 +30,8 @@ public class Tooth.AttachmentsPage : ComposerPage {
|
|||
};
|
||||
|
||||
public GLib.ListStore attachments;
|
||||
public Adw.ToastOverlay toast_overlay;
|
||||
public bool can_publish { get; set; default = false; }
|
||||
|
||||
public AttachmentsPage () {
|
||||
Object (
|
||||
|
@ -52,6 +54,7 @@ public class Tooth.AttachmentsPage : ComposerPage {
|
|||
var attach_button = new Button.with_label (_("Add Media")) {
|
||||
halign = Align.CENTER
|
||||
};
|
||||
attach_button.add_css_class("pill");
|
||||
attach_button.clicked.connect (show_file_selector);
|
||||
|
||||
empty_state = new Adw.StatusPage () {
|
||||
|
@ -67,11 +70,25 @@ public class Tooth.AttachmentsPage : ComposerPage {
|
|||
list = new ListBox ();
|
||||
list.bind_model (attachments, on_create_list_item);
|
||||
|
||||
var add_media_action_button = new Gtk.Button() {
|
||||
icon_name = "tooth-plus-large-symbolic",
|
||||
valign = Gtk.Align.CENTER,
|
||||
halign = Gtk.Align.CENTER
|
||||
};
|
||||
add_media_action_button.add_css_class ("flat");
|
||||
add_media_action_button.clicked.connect(show_file_selector);
|
||||
|
||||
bottom_bar.pack_start (add_media_action_button);
|
||||
|
||||
// State stack
|
||||
stack = new Adw.ViewStack ();
|
||||
stack.add_named (list, "list");
|
||||
stack.add_named (empty_state, "empty");
|
||||
content.prepend (stack);
|
||||
|
||||
toast_overlay = new Adw.ToastOverlay();
|
||||
toast_overlay.child = stack;
|
||||
|
||||
content.prepend (toast_overlay);
|
||||
}
|
||||
|
||||
public override void on_pull () {
|
||||
|
@ -80,19 +97,39 @@ public class Tooth.AttachmentsPage : ComposerPage {
|
|||
|
||||
Widget on_create_list_item (Object item) {
|
||||
var attachment = item as API.Attachment;
|
||||
return new Label (attachment.source_file.get_uri ());
|
||||
var attachment_widget = new AttachmentsPageAttachment(attachment.id, attachment.source_file, dialog);
|
||||
attachment_widget.remove_from_model.connect(() => {
|
||||
uint indx;
|
||||
var found = attachments.find (item, out indx);
|
||||
if (found)
|
||||
attachments.remove(indx);
|
||||
});
|
||||
return attachment_widget;
|
||||
}
|
||||
|
||||
void on_attachments_changed () {
|
||||
var is_empty = attachments.get_n_items () < 1;
|
||||
stack.visible_child_name = (is_empty ? "empty" : "list");
|
||||
if (is_empty) {
|
||||
stack.visible_child_name = "empty";
|
||||
bottom_bar.hide ();
|
||||
can_publish = false;
|
||||
} else {
|
||||
stack.visible_child_name = "list";
|
||||
bottom_bar.show ();
|
||||
can_publish = true;
|
||||
}
|
||||
}
|
||||
|
||||
void show_file_selector () {
|
||||
var filter = new FileFilter () {
|
||||
name = _("All Supported Files")
|
||||
};
|
||||
foreach (var mime_type in SUPPORTED_MIMES) {
|
||||
|
||||
var supported_mimes = new Gee.ArrayList<string>.wrap(SUPPORTED_MIMES);
|
||||
if (accounts.active.instance_info != null && accounts.active.instance_info.configuration != null && accounts.active.instance_info.configuration.media_attachments != null && accounts.active.instance_info.configuration.media_attachments.supported_mime_types != null && accounts.active.instance_info.configuration.media_attachments.supported_mime_types.size > 0) {
|
||||
supported_mimes = accounts.active.instance_info.configuration.media_attachments.supported_mime_types;
|
||||
}
|
||||
foreach (var mime_type in supported_mimes) {
|
||||
filter.add_mime_type (mime_type);
|
||||
}
|
||||
|
||||
|
@ -104,10 +141,50 @@ public class Tooth.AttachmentsPage : ComposerPage {
|
|||
switch (id) {
|
||||
case ResponseType.ACCEPT:
|
||||
var files = chooser.get_files ();
|
||||
for (var i = 0; i < chooser.get_files ().get_n_items (); i++) {
|
||||
for (var i = 0; i < files.get_n_items (); i++) {
|
||||
var file = files.get_item (i) as File;
|
||||
var attachment = API.Attachment.upload (file);
|
||||
attachments.append (attachment);
|
||||
|
||||
if (accounts.active.instance_info.compat_status_max_image_size > 0) {
|
||||
try {
|
||||
var file_info = file.query_info ("standard::size,standard::content-type", 0);
|
||||
var file_content_type = file_info.get_content_type ();
|
||||
|
||||
if (file_content_type != null) {
|
||||
file_content_type = file_content_type.down();
|
||||
var file_size = file_info.get_size();
|
||||
var skip = (file_content_type.contains("image/") &&
|
||||
file_size >= accounts.active.instance_info.compat_status_max_image_size) ||
|
||||
(file_content_type.contains("video/") &&
|
||||
file_size >= accounts.active.instance_info.compat_status_max_video_size);
|
||||
|
||||
if (skip) {
|
||||
var toast = new Adw.Toast(_("File \"%s\" is bigger than the instance limit").printf(file.get_basename())) {
|
||||
timeout = 0
|
||||
};
|
||||
toast_overlay.add_toast(toast);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Error e) {
|
||||
warning (e.message);
|
||||
}
|
||||
}
|
||||
|
||||
API.Attachment.upload.begin (file.get_uri (), (obj, res) => {
|
||||
try {
|
||||
var attachment = API.Attachment.upload.end (res);
|
||||
attachment.source_file = file;
|
||||
attachments.append (attachment);
|
||||
}
|
||||
catch (Error e) {
|
||||
warning (e.message);
|
||||
var toast = new Adw.Toast(e.message) {
|
||||
timeout = 0
|
||||
};
|
||||
toast_overlay.add_toast(toast);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -117,4 +194,12 @@ public class Tooth.AttachmentsPage : ComposerPage {
|
|||
chooser.show ();
|
||||
}
|
||||
|
||||
public override void on_modify_req (Request req) {
|
||||
if (can_publish){
|
||||
for (var i = 0; i < attachments.get_n_items (); i++) {
|
||||
var attachment = attachments.get_item (i) as API.Attachment;
|
||||
req.with_form_data ("media_ids[]", attachment.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
public class Tooth.AttachmentsPageAttachment : Widgets.Attachment.Item {
|
||||
|
||||
protected Gtk.Picture pic;
|
||||
protected File attachment_file;
|
||||
protected string? alt_text { get; set; default = null; }
|
||||
private const int ALT_MAX_CHARS = 1500;
|
||||
private Dialogs.Compose compose_dialog;
|
||||
protected string id;
|
||||
|
||||
public AttachmentsPageAttachment (string attachment_id, File file, Dialogs.Compose dialog){
|
||||
id = attachment_id;
|
||||
attachment_file = file;
|
||||
compose_dialog = dialog;
|
||||
pic = new Gtk.Picture.for_file (file) {
|
||||
hexpand = true,
|
||||
vexpand = true,
|
||||
can_shrink = true,
|
||||
keep_aspect_ratio = true
|
||||
};
|
||||
button.child = pic;
|
||||
badge.visible = false;
|
||||
alt_btn.disconnect(alt_btn_clicked_id);
|
||||
alt_btn.clicked.connect(() => {
|
||||
create_alt_text_input_window().show();
|
||||
});
|
||||
alt_btn.add_css_class("error");
|
||||
alt_btn.remove_css_class("flat");
|
||||
|
||||
var delete_button = new Gtk.Button() {
|
||||
icon_name = "tooth-trash-symbolic",
|
||||
valign = Gtk.Align.CENTER,
|
||||
halign = Gtk.Align.CENTER
|
||||
};
|
||||
badge_box.append(delete_button);
|
||||
delete_button.add_css_class("error");
|
||||
|
||||
delete_button.clicked.connect(() => remove_from_model());
|
||||
}
|
||||
|
||||
public virtual signal void remove_from_model () {}
|
||||
|
||||
protected override void on_rebind () {}
|
||||
|
||||
protected override void on_secondary_click () {}
|
||||
|
||||
protected override void on_click () {
|
||||
Host.open_uri (attachment_file.get_path ());
|
||||
}
|
||||
|
||||
protected bool validate(int text_size) {
|
||||
// text_size > 0 &&
|
||||
return text_size <= ALT_MAX_CHARS;
|
||||
}
|
||||
|
||||
protected string remaining_alt_chars(int text_size) {
|
||||
return (ALT_MAX_CHARS - text_size).to_string();
|
||||
}
|
||||
|
||||
protected Adw.Window create_alt_text_input_window () {
|
||||
var alt_editor = new Gtk.TextView () {
|
||||
vexpand = true,
|
||||
hexpand = true,
|
||||
top_margin = 6,
|
||||
right_margin = 6,
|
||||
bottom_margin = 6,
|
||||
left_margin = 6,
|
||||
pixels_below_lines = 6,
|
||||
accepts_tab = false,
|
||||
wrap_mode = Gtk.WrapMode.WORD_CHAR
|
||||
};
|
||||
var scroller = new Gtk.ScrolledWindow () {
|
||||
hexpand = true,
|
||||
vexpand = true
|
||||
};
|
||||
scroller.child = alt_editor;
|
||||
|
||||
var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
|
||||
var headerbar = new Adw.HeaderBar();
|
||||
|
||||
var bottom_bar = new Gtk.ActionBar ();
|
||||
var char_counter = new Gtk.Label (remaining_alt_chars(alt_text != null ? alt_text.length : 0)) {
|
||||
margin_end = 6,
|
||||
margin_top = 14,
|
||||
margin_bottom = 14,
|
||||
tooltip_text = _("Characters Left")
|
||||
};
|
||||
char_counter.add_css_class ("heading");
|
||||
bottom_bar.pack_end (char_counter);
|
||||
|
||||
var save_btn = new Gtk.Button.with_label(_("Save"));
|
||||
save_btn.add_css_class("suggested-action");
|
||||
|
||||
save_btn.sensitive = alt_text != null && validate(alt_text.length);
|
||||
|
||||
headerbar.pack_end(save_btn);
|
||||
|
||||
box.append(headerbar);
|
||||
box.append(scroller);
|
||||
box.append(bottom_bar);
|
||||
|
||||
if (alt_text != null)
|
||||
alt_editor.buffer.text = alt_text;
|
||||
alt_editor.buffer.changed.connect (() => {
|
||||
var t_val = validate(alt_editor.buffer.get_char_count ());
|
||||
save_btn.sensitive = t_val;
|
||||
char_counter.label = remaining_alt_chars(alt_editor.buffer.get_char_count ());
|
||||
if (t_val) {
|
||||
char_counter.remove_css_class ("error");
|
||||
} else {
|
||||
char_counter.add_css_class ("error");
|
||||
}
|
||||
});
|
||||
|
||||
var dialog = new Adw.Window() {
|
||||
modal = true,
|
||||
title = @"Alternative text for attachment",
|
||||
transient_for = compose_dialog,
|
||||
content = box,
|
||||
default_width = 400,
|
||||
default_height = 300
|
||||
};
|
||||
|
||||
save_btn.clicked.connect(() => {
|
||||
alt_text = alt_editor.buffer.text;
|
||||
if (validate(alt_editor.buffer.get_char_count ()) && alt_editor.buffer.get_char_count () > 0) {
|
||||
alt_btn.add_css_class("success");
|
||||
alt_btn.remove_css_class("error");
|
||||
} else {
|
||||
alt_btn.remove_css_class("success");
|
||||
alt_btn.add_css_class("error");
|
||||
}
|
||||
new Request.PUT (@"/api/v1/media/$(id)")
|
||||
.with_account (accounts.active)
|
||||
.with_param ("description", HtmlUtils.uri_encode (alt_text))
|
||||
.then(() => {})
|
||||
.exec ();
|
||||
dialog.destroy();
|
||||
});
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
|
@ -29,10 +29,18 @@ public class Tooth.Dialogs.Compose : Adw.Window {
|
|||
|
||||
protected virtual signal void build () {
|
||||
var p_edit = new EditorPage ();
|
||||
p_edit.bind_property("can-publish", commit_button, "sensitive", BindingFlags.SYNC_CREATE);
|
||||
var p_attach = new AttachmentsPage ();
|
||||
p_edit.bind_property("can-publish", commit_button, "sensitive", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
|
||||
target.set_boolean (src.get_boolean() || p_attach.can_publish);
|
||||
return true;
|
||||
});
|
||||
p_attach.bind_property("can-publish", commit_button, "sensitive", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
|
||||
target.set_boolean (src.get_boolean() || p_edit.can_publish);
|
||||
return true;
|
||||
});
|
||||
|
||||
add_page (p_edit);
|
||||
add_page (new AttachmentsPage ());
|
||||
add_page (p_attach);
|
||||
add_page (new PollPage ());
|
||||
}
|
||||
|
||||
|
|
|
@ -2,13 +2,18 @@ using Gtk;
|
|||
|
||||
public class Tooth.EditorPage : ComposerPage {
|
||||
|
||||
protected uint char_limit { get; set; default = 500; } //TODO: Ask the instance to get this value
|
||||
protected int remaining_chars { get; set; default = 0; }
|
||||
protected int64 char_limit { get; set; default = 500; }
|
||||
protected int64 remaining_chars { get; set; default = 0; }
|
||||
public bool can_publish { get; set; default = false; }
|
||||
|
||||
construct {
|
||||
title = _("Text");
|
||||
icon_name = "tooth-edit-symbolic";
|
||||
|
||||
var char_limit_api = accounts.active.instance_info.compat_status_max_characters;
|
||||
if (char_limit_api > 0)
|
||||
char_limit = char_limit_api;
|
||||
remaining_chars = char_limit;
|
||||
}
|
||||
|
||||
public override void on_build (Dialogs.Compose dialog, API.Status status) {
|
||||
|
@ -46,7 +51,8 @@ public class Tooth.EditorPage : ComposerPage {
|
|||
}
|
||||
|
||||
public override void on_modify_req (Request req) {
|
||||
req.with_form_data ("status", status.content);
|
||||
if (can_publish)
|
||||
req.with_form_data ("status", status.content);
|
||||
req.with_form_data ("visibility", status.visibility);
|
||||
|
||||
if (dialog.status.in_reply_to_id != null)
|
||||
|
@ -67,7 +73,7 @@ public class Tooth.EditorPage : ComposerPage {
|
|||
|
||||
protected void install_editor () {
|
||||
recount_chars.connect (() => {
|
||||
remaining_chars = (int) char_limit;
|
||||
remaining_chars = char_limit;
|
||||
});
|
||||
recount_chars.connect_after (() => {
|
||||
placeholder.visible = remaining_chars == char_limit;
|
||||
|
|
|
@ -24,5 +24,4 @@ public class Tooth.Widgets.Attachment.Image : Widgets.Attachment.Item {
|
|||
protected virtual void on_cache_response (bool is_loaded, owned Paintable? data) {
|
||||
pic.paintable = data;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ public class Tooth.Widgets.Attachment.Item : Adw.Bin {
|
|||
protected Button button;
|
||||
protected Button alt_btn;
|
||||
protected Label badge;
|
||||
protected Gtk.Box badge_box;
|
||||
protected ulong alt_btn_clicked_id;
|
||||
|
||||
private void copy_url () {
|
||||
Host.copy (entity.url);
|
||||
|
@ -87,7 +89,7 @@ public class Tooth.Widgets.Attachment.Item : Adw.Bin {
|
|||
gesture_click_controller.pressed.connect(on_secondary_click);
|
||||
gesture_lp_controller.pressed.connect(on_secondary_click);
|
||||
|
||||
var badge_box = new Gtk.Box(Orientation.HORIZONTAL, 1) {
|
||||
badge_box = new Gtk.Box(Orientation.HORIZONTAL, 1) {
|
||||
valign = Align.END,
|
||||
halign = Align.START
|
||||
};
|
||||
|
@ -98,7 +100,7 @@ public class Tooth.Widgets.Attachment.Item : Adw.Bin {
|
|||
alt_btn.add_css_class ("heading");
|
||||
alt_btn.add_css_class ("flat");
|
||||
|
||||
alt_btn.clicked.connect(() => {
|
||||
alt_btn_clicked_id = alt_btn.clicked.connect(() => {
|
||||
if (entity != null && entity.description != null)
|
||||
create_alt_text_window(entity.description).show();
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue