diff --git a/data/ui/dialogs/compose.ui b/data/ui/dialogs/compose.ui
index bb6bf37..37aad2a 100644
--- a/data/ui/dialogs/compose.ui
+++ b/data/ui/dialogs/compose.ui
@@ -26,7 +26,7 @@
diff --git a/src/API/Attachment.vala b/src/API/Attachment.vala
index a509d35..88bc5bb 100644
--- a/src/API/Attachment.vala
+++ b/src/API/Attachment.vala
@@ -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 (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 (node);
+ // message (@"OK! ID $(entity.id)");
+ // return entity;
+ // }
+ // }
+
public override Gtk.Widget to_widget () {
if (preview_url != null) {
return new Widgets.Attachment.Image () {
diff --git a/src/Dialogs/Composer/AttachmentsPage.vala b/src/Dialogs/Composer/AttachmentsPage.vala
index b8190da..70c0d0b 100644
--- a/src/Dialogs/Composer/AttachmentsPage.vala
+++ b/src/Dialogs/Composer/AttachmentsPage.vala
@@ -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 ();
}
}
diff --git a/src/Dialogs/Composer/Dialog.vala b/src/Dialogs/Composer/Dialog.vala
index 3f38bd2..df6bb45 100644
--- a/src/Dialogs/Composer/Dialog.vala
+++ b/src/Dialogs/Composer/Dialog.vala
@@ -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);
diff --git a/src/Dialogs/Composer/EditorPage.vala b/src/Dialogs/Composer/EditorPage.vala
index 12231b8..fdf8faa 100644
--- a/src/Dialogs/Composer/EditorPage.vala
+++ b/src/Dialogs/Composer/EditorPage.vala
@@ -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) {
diff --git a/src/Dialogs/Composer/Page.vala b/src/Dialogs/Composer/Page.vala
index ac3adce..4834e3d 100644
--- a/src/Dialogs/Composer/Page.vala
+++ b/src/Dialogs/Composer/Page.vala
@@ -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) {}
diff --git a/src/Services/Network/Network.vala b/src/Services/Network/Network.vala
index da57062..b47df90 100644
--- a/src/Services/Network/Network.vala
+++ b/src/Services/Network/Network.vala
@@ -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);
});
- }
+ }
}