Composer: Initial work for attachments
This commit is contained in:
parent
0c10bee64d
commit
ddba63d805
|
@ -26,7 +26,7 @@
|
|||
</child>
|
||||
<child type="start">
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="label" translatable="yes">Close</property>
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
|
|
|
@ -1,33 +1,5 @@
|
|||
public class Tootle.API.Attachment : Entity, Widgetizable {
|
||||
|
||||
// https://github.com/tootsuite/mastodon/blob/master/app/models/media_attachment.rb
|
||||
public const string[] SUPPORTED_MIMES = {
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"video/webm",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"video/ogg",
|
||||
"video/webm",
|
||||
"video/quicktime",
|
||||
"audio/wave",
|
||||
"audio/wav",
|
||||
"audio/x-wav",
|
||||
"audio/x-pn-wave",
|
||||
"audio/ogg",
|
||||
"audio/mpeg",
|
||||
"audio/mp3",
|
||||
"audio/webm",
|
||||
"audio/flac",
|
||||
"audio/aac",
|
||||
"audio/m4a",
|
||||
"audio/x-m4a",
|
||||
"audio/mp4",
|
||||
"audio/3gpp",
|
||||
"video/x-ms-asf"
|
||||
};
|
||||
|
||||
public string id { get; set; }
|
||||
public string kind { get; set; default = "unknown"; }
|
||||
public string url { get; set; }
|
||||
|
@ -38,59 +10,73 @@ public class Tootle.API.Attachment : Entity, Widgetizable {
|
|||
get { return (this._preview_url == null || this._preview_url == "") ? url : _preview_url; }
|
||||
}
|
||||
|
||||
public static async Attachment upload (string uri, string title, string? descr) throws Error {
|
||||
message (@"Uploading new media: $(uri)...");
|
||||
public File? source_file { get; set; }
|
||||
|
||||
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 bool is_published {
|
||||
get {
|
||||
return this.source_file == null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Attachment upload (File file) {
|
||||
return new Attachment () {
|
||||
source_file = file
|
||||
};
|
||||
}
|
||||
|
||||
// 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 () {
|
||||
|
|
|
@ -1,10 +1,120 @@
|
|||
using Gtk;
|
||||
|
||||
public class Tootle.AttachmentsPage : ComposerPage {
|
||||
|
||||
// https://github.com/tootsuite/mastodon/blob/master/app/models/media_attachment.rb
|
||||
public const string[] SUPPORTED_MIMES = {
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"video/webm",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"video/ogg",
|
||||
"video/webm",
|
||||
"audio/wave",
|
||||
"audio/wav",
|
||||
"audio/x-wav",
|
||||
"audio/x-pn-wave",
|
||||
"audio/ogg",
|
||||
"audio/mpeg",
|
||||
"audio/mp3",
|
||||
"audio/webm",
|
||||
"audio/flac",
|
||||
"audio/aac",
|
||||
"audio/m4a",
|
||||
"audio/x-m4a",
|
||||
"audio/mp4",
|
||||
"audio/3gpp",
|
||||
"video/x-ms-asf"
|
||||
};
|
||||
|
||||
public GLib.ListStore attachments;
|
||||
|
||||
public AttachmentsPage () {
|
||||
Object (
|
||||
title: _("Media"),
|
||||
icon_name: "mail-attachment-symbolic"
|
||||
);
|
||||
|
||||
attachments = new GLib.ListStore (typeof (API.Attachment));
|
||||
attachments.items_changed.connect (on_attachments_changed);
|
||||
}
|
||||
|
||||
protected Adw.ViewStack stack;
|
||||
protected Adw.StatusPage empty_state;
|
||||
protected ListBox list;
|
||||
|
||||
public override void on_build (Dialogs.Compose dialog, API.Status status) {
|
||||
base.on_build (dialog, status);
|
||||
|
||||
// Empty state
|
||||
var attach_button = new Button.with_label (_("Add Media")) {
|
||||
halign = Align.CENTER
|
||||
};
|
||||
attach_button.clicked.connect (show_file_selector);
|
||||
|
||||
empty_state = new Adw.StatusPage () {
|
||||
title = _("No Media"),
|
||||
description = _("Drag files here or click the button below"),
|
||||
vexpand = true,
|
||||
icon_name = icon_name,
|
||||
child = attach_button
|
||||
};
|
||||
empty_state.add_css_class ("compact");
|
||||
|
||||
// Non-empty state
|
||||
list = new ListBox ();
|
||||
list.bind_model (attachments, on_create_list_item);
|
||||
|
||||
// State stack
|
||||
stack = new Adw.ViewStack ();
|
||||
stack.add_named (list, "list");
|
||||
stack.add_named (empty_state, "empty");
|
||||
content.prepend (stack);
|
||||
}
|
||||
|
||||
public override void on_pull () {
|
||||
on_attachments_changed ();
|
||||
}
|
||||
|
||||
Widget on_create_list_item (Object item) {
|
||||
var attachment = item as API.Attachment;
|
||||
return new Label (attachment.source_file.get_uri ());
|
||||
}
|
||||
|
||||
void on_attachments_changed () {
|
||||
var is_empty = attachments.get_n_items () < 1;
|
||||
stack.visible_child_name = (is_empty ? "empty" : "list");
|
||||
}
|
||||
|
||||
void show_file_selector () {
|
||||
var filter = new FileFilter () {
|
||||
name = _("All Supported Files")
|
||||
};
|
||||
foreach (var mime_type in SUPPORTED_MIMES) {
|
||||
filter.add_mime_type (mime_type);
|
||||
}
|
||||
|
||||
var chooser = new FileChooserNative (_("Open"), dialog, Gtk.FileChooserAction.OPEN, null, null) {
|
||||
select_multiple = true,
|
||||
filter = filter
|
||||
};
|
||||
chooser.response.connect (id => {
|
||||
switch (id) {
|
||||
case ResponseType.ACCEPT:
|
||||
var files = chooser.get_files ();
|
||||
for (var i = 0; i < chooser.get_files ().get_n_items (); i++) {
|
||||
var file = files.get_item (i) as File;
|
||||
var attachment = API.Attachment.upload (file);
|
||||
attachments.append (attachment);
|
||||
}
|
||||
break;
|
||||
}
|
||||
chooser.unref ();
|
||||
});
|
||||
chooser.ref ();
|
||||
chooser.show ();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,13 +13,17 @@ public class Tootle.Dialogs.Compose : Adw.Window {
|
|||
set { commit_button.add_css_class (value); }
|
||||
}
|
||||
|
||||
ulong build_sigid;
|
||||
|
||||
construct {
|
||||
transient_for = app.main_window;
|
||||
title_switcher.stack = stack;
|
||||
|
||||
notify["status"].connect (() => {
|
||||
build_sigid = notify["status"].connect (() => {
|
||||
build ();
|
||||
present ();
|
||||
|
||||
disconnect (build_sigid);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -80,7 +84,9 @@ public class Tootle.Dialogs.Compose : Adw.Window {
|
|||
protected void add_page (ComposerPage page) {
|
||||
var wrapper = stack.add (page);
|
||||
page.on_build (this, this.status);
|
||||
modify_req.connect (page.on_sync);
|
||||
page.on_pull ();
|
||||
|
||||
modify_req.connect (page.on_push);
|
||||
modify_req.connect (page.on_modify_req);
|
||||
page.bind_property ("visible", wrapper, "visible", GLib.BindingFlags.SYNC_CREATE);
|
||||
page.bind_property ("title", wrapper, "title", GLib.BindingFlags.SYNC_CREATE);
|
||||
|
|
|
@ -14,7 +14,6 @@ public class Tootle.EditorPage : ComposerPage {
|
|||
base.on_build (dialog, status);
|
||||
|
||||
install_editor ();
|
||||
populate_editor ();
|
||||
install_visibility ();
|
||||
install_cw ();
|
||||
|
||||
|
@ -27,9 +26,11 @@ public class Tootle.EditorPage : ComposerPage {
|
|||
recount_chars ();
|
||||
}
|
||||
|
||||
public override void on_sync () {
|
||||
warning ("syncing");
|
||||
public override void on_pull () {
|
||||
populate_editor ();
|
||||
}
|
||||
|
||||
public override void on_push () {
|
||||
status.content = editor.buffer.text;
|
||||
status.sensitive = cw_button.active;
|
||||
if (status.sensitive) {
|
||||
|
|
|
@ -40,14 +40,16 @@ public class Tootle.ComposerPage : Gtk.Box {
|
|||
bottom_bar.show ();
|
||||
}
|
||||
|
||||
// This is used to populate the UI with the status entity data
|
||||
public virtual void on_build (Dialogs.Compose dialog, API.Status status) {
|
||||
this.dialog = dialog;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
// This is used to push data back to the status entity
|
||||
public virtual void on_sync () {}
|
||||
// Entity -> UI state
|
||||
public virtual void on_pull () {}
|
||||
|
||||
// UI state -> Entity
|
||||
public virtual void on_push () {}
|
||||
|
||||
public virtual void on_modify_req (Request req) {}
|
||||
|
||||
|
|
|
@ -5,92 +5,92 @@ using Json;
|
|||
|
||||
public class Tootle.Network : GLib.Object {
|
||||
|
||||
public signal void started ();
|
||||
public signal void finished ();
|
||||
public signal void started ();
|
||||
public signal void finished ();
|
||||
|
||||
public delegate void ErrorCallback (int32 code, string reason);
|
||||
public delegate void SuccessCallback (Session session, Message msg) throws Error;
|
||||
public delegate void NodeCallback (Json.Node node, Message msg) throws Error;
|
||||
public delegate void ObjectCallback (Json.Object node) throws Error;
|
||||
|
||||
private int requests_processing = 0;
|
||||
public Soup.Session session;
|
||||
public Soup.Session session { get; set; }
|
||||
int requests_processing = 0;
|
||||
|
||||
construct {
|
||||
session = new Soup.Session () {
|
||||
ssl_strict = true,
|
||||
ssl_use_system_ca_file = true
|
||||
};
|
||||
session.request_unqueued.connect (msg => {
|
||||
requests_processing--;
|
||||
if (requests_processing <= 0)
|
||||
finished ();
|
||||
});
|
||||
}
|
||||
construct {
|
||||
session = new Soup.Session () {
|
||||
ssl_strict = true,
|
||||
ssl_use_system_ca_file = true
|
||||
};
|
||||
session.request_unqueued.connect (msg => {
|
||||
requests_processing--;
|
||||
if (requests_processing <= 0)
|
||||
finished ();
|
||||
});
|
||||
}
|
||||
|
||||
public void cancel (Soup.Message? msg) {
|
||||
if (msg == null)
|
||||
return;
|
||||
public void cancel (Soup.Message? msg) {
|
||||
if (msg == null)
|
||||
return;
|
||||
|
||||
switch (msg.status_code) {
|
||||
case Soup.Status.CANCELLED:
|
||||
case Soup.Status.OK:
|
||||
return;
|
||||
}
|
||||
switch (msg.status_code) {
|
||||
case Soup.Status.CANCELLED:
|
||||
case Soup.Status.OK:
|
||||
return;
|
||||
}
|
||||
|
||||
debug ("Cancelling message");
|
||||
session.cancel_message (msg, Soup.Status.CANCELLED);
|
||||
}
|
||||
debug ("Cancelling message");
|
||||
session.cancel_message (msg, Soup.Status.CANCELLED);
|
||||
}
|
||||
|
||||
public void queue (owned Soup.Message mess, owned SuccessCallback cb, owned ErrorCallback ecb) {
|
||||
requests_processing++;
|
||||
started ();
|
||||
public void queue (owned Soup.Message mess, owned SuccessCallback cb, owned ErrorCallback ecb) {
|
||||
requests_processing++;
|
||||
started ();
|
||||
|
||||
message (@"$(mess.method): $(mess.uri.to_string (false))");
|
||||
|
||||
try {
|
||||
session.queue_message (mess, (sess, msg) => {
|
||||
var status = msg.status_code;
|
||||
if (status == Soup.Status.OK)
|
||||
cb (session, msg);
|
||||
else if (status == Soup.Status.CANCELLED)
|
||||
debug ("Message is cancelled. Ignoring callback invocation.");
|
||||
else
|
||||
ecb ((int32) status, describe_error ((int32) status));
|
||||
});
|
||||
}
|
||||
catch (Error e) {
|
||||
warning (@"Exception in network queue: $(e.message)");
|
||||
ecb (0, e.message);
|
||||
}
|
||||
}
|
||||
try {
|
||||
session.queue_message (mess, (sess, msg) => {
|
||||
var status = msg.status_code;
|
||||
if (status == Soup.Status.OK)
|
||||
cb (session, msg);
|
||||
else if (status == Soup.Status.CANCELLED)
|
||||
debug ("Message is cancelled. Ignoring callback invocation.");
|
||||
else
|
||||
ecb ((int32) status, describe_error ((int32) status));
|
||||
});
|
||||
}
|
||||
catch (Error e) {
|
||||
warning (@"Exception in network queue: $(e.message)");
|
||||
ecb (0, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
public string describe_error (uint code) {
|
||||
var reason = Soup.Status.get_phrase (code);
|
||||
var reason = Soup.Status.get_phrase (code);
|
||||
return @"$code: $reason";
|
||||
}
|
||||
|
||||
public void on_error (int32 code, string message) {
|
||||
warning (message);
|
||||
app.toast (message);
|
||||
}
|
||||
public void on_error (int32 code, string message) {
|
||||
warning (message);
|
||||
app.toast (message);
|
||||
}
|
||||
|
||||
public Json.Node parse_node (Soup.Message msg) throws Error {
|
||||
var parser = new Json.Parser ();
|
||||
parser.load_from_data ((string) msg.response_body.flatten ().data, -1);
|
||||
return parser.get_root ();
|
||||
}
|
||||
public Json.Node parse_node (Soup.Message msg) throws Error {
|
||||
var parser = new Json.Parser ();
|
||||
parser.load_from_data ((string) msg.response_body.flatten ().data, -1);
|
||||
return parser.get_root ();
|
||||
}
|
||||
|
||||
public Json.Object parse (Soup.Message msg) throws Error {
|
||||
return parse_node (msg).get_object ();
|
||||
}
|
||||
public Json.Object parse (Soup.Message msg) throws Error {
|
||||
return parse_node (msg).get_object ();
|
||||
}
|
||||
|
||||
public static void parse_array (Soup.Message msg, owned NodeCallback cb) throws Error {
|
||||
public static void parse_array (Soup.Message msg, owned NodeCallback cb) throws Error {
|
||||
var parser = new Json.Parser ();
|
||||
parser.load_from_data ((string) msg.response_body.flatten ().data, -1);
|
||||
parser.get_root ().get_array ().foreach_element ((array, i, node) => {
|
||||
cb (node, msg);
|
||||
cb (node, msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue