using Gtk; using Adw; using Invercargill; namespace Publicate { public class PublishWindow : Adw.Window { private Adw.HeaderBar header_bar; private ViewerWindow window; private Stack stack; private Box loader; private Label loader_status; private ProgressBar loader_progress; private bool loader_pulsing = false; private Box identity_select; private Label identity_header; private IdentityList identity_list; private Button publish_button; private Box decision; private Label decision_heading; private Label decision_body; private Button cancel_button; private Button continue_button; private Box error_view; private Label error_heading; private Label error_body; private Box complete; private StatusPage status_page; private delegate void DecisionCallback(); private DecisionCallback cancel_action; private DecisionCallback continue_action; public PublishWindow(ViewerWindow window) { this.window = window; modal = true; transient_for = window; var box = new Box(Orientation.VERTICAL, 0); header_bar = new Adw.HeaderBar(); header_bar.show_end_title_buttons = false; title = "Publish"; default_height = 500; default_width = 700; box.append (header_bar); stack = new Stack(); stack.vexpand = true; stack.transition_type = StackTransitionType.CROSSFADE; box.append(stack); content = box; loader = new Box(Orientation.VERTICAL, 8); loader_status = new Label ("Please wait…"); loader_status.halign = Align.START; loader_progress = new ProgressBar (); loader_progress.hexpand = true; loader.append (loader_status); loader.append (loader_progress); loader.width_request = 550; loader.valign = Align.CENTER; loader.halign = Align.CENTER; stack.add_child (loader); stack.visible_child = loader; identity_select = new Box(Orientation.VERTICAL, 8); identity_select.margin_top = 18; identity_select.margin_start = 18; identity_select.margin_end = 18; identity_select.margin_bottom = 18; identity_header = new Label("Select identity to publish with"); identity_header.halign = Align.START; identity_header.add_css_class("title-2"); identity_select.append(identity_header); identity_list = new IdentityList(window); identity_select.append(identity_list); publish_button = new Button.with_label("Publish"); publish_button.add_css_class("suggested-action"); publish_button.halign = Align.END; publish_button.clicked.connect(() => continue_action()); identity_select.append(publish_button); stack.add_child(identity_select); error_view = new Box(Orientation.VERTICAL, 8); error_view.halign = Align.CENTER; error_view.valign = Align.CENTER; error_view.margin_top = 18; error_view.margin_start = 64; error_view.margin_end = 64; error_view.margin_bottom = 18; error_heading.width_request = 200; error_heading = new Label("Decision"); error_heading.halign = Align.START; error_heading.add_css_class("title-2"); error_view.append(error_heading); error_body = new Label("Details"); error_body.halign = Align.START; error_body.hexpand = true; error_body.wrap = true; error_body.wrap_mode = Pango.WrapMode.WORD; error_body.natural_wrap_mode = NaturalWrapMode.WORD; error_view.append(error_body); var error_close_button = new Button.with_label("Close"); error_close_button.hexpand = true; error_close_button.clicked.connect(() => close()); error_view.append(error_close_button); stack.add_child(error_view); decision = new Box(Orientation.VERTICAL, 8); decision.halign = Align.CENTER; decision.valign = Align.CENTER; decision.margin_top = 18; decision.margin_start = 64; decision.margin_end = 64; decision.margin_bottom = 18; decision.width_request = 200; decision_heading = new Label("Decision"); decision_heading.halign = Align.START; decision_heading.add_css_class("title-2"); decision.append(decision_heading); decision_body = new Label("Details"); decision_body.halign = Align.START; decision_body.hexpand = true; decision_body.wrap = true; decision_body.wrap_mode = Pango.WrapMode.WORD; decision_body.natural_wrap_mode = NaturalWrapMode.WORD; decision.append(decision_body); var button_box = new Box(Orientation.HORIZONTAL, 0); button_box.add_css_class("linked"); cancel_button = new Button.with_label("Cancel"); cancel_button.hexpand = true; cancel_button.clicked.connect(() => cancel_action()); button_box.append(cancel_button); continue_button = new Button.with_label("Continue"); continue_button.hexpand = true; continue_button.clicked.connect(() => continue_action()); button_box.append(continue_button); decision.append(button_box); stack.add_child(decision); complete = new Box(Orientation.VERTICAL, 8); complete.halign = Align.CENTER; complete.valign = Align.CENTER; complete.margin_top = 18; complete.margin_start = 18; complete.margin_end = 18; complete.margin_bottom = 18; status_page = new StatusPage(); complete.append(status_page); status_page.icon_name = "emblem-ok-symbolic"; status_page.title = "Publish Completed!"; var close_button = new Button.with_label("Close"); status_page.child = close_button; close_button.width_request = 400; close_button.clicked.connect(() => close()); stack.add_child(complete); } private void pulse_loader() { if(loader_pulsing) { return; } loader_pulsing = true; Timeout.add(100, () => { if(!loader_pulsing) { return false; } loader_progress.pulse(); return true; }); } private void show_decision(string heading, string body, bool is_destructive, DecisionCallback cancel_cb, DecisionCallback continue_cb) { decision_heading.set_text(heading); decision_body.set_text(body); continue_button.set_css_classes(new string[] { is_destructive ? "destructive-action" : "suggested-action"}); continue_action = continue_cb; cancel_action = cancel_cb; stack.visible_child = decision; } private void show_error(string heading, string body) { error_heading.set_text(heading); error_body.set_text(body); stack.visible_child = error_view; error_bell(); } private Ppub.Collection collection; private CollectionConfig config; private Pprf.Client client; private File publication_file; private string dest_name; private BinaryData cid; private bool unpublish = false; private DateTime? old_publish_timestamp = null; public async void publish_to(CollectionConfig config, File publication_file) { title = @"Publish to $(config.name)"; this.config = config; this.publication_file = publication_file; dest_name = publication_file.get_basename(); if(!dest_name.has_suffix(".ppub")) { dest_name += ".ppub"; } pulse_loader(); loader_status.set_text(@"Looking up $(config.domain)…"); try{ client = yield window.collection_service.get_client(config); } catch(Error e) { show_error("Server not found", @"Could not find PPRF server details for $(config.domain): $(e.message)"); return; } cid = new ByteComposition.from_base64(config.collection_id); loader_status.set_text(@"Querying $(config.domain) for collection information…"); try { collection = yield do_in_bg(() => client.get_collection(cid)); } catch(Error e) { show_error("Could not download collection information", @"Failed to download collection information from server: $(e.message)"); return; } loader_pulsing = false; loader_progress.fraction = 0; if(collection.publications.any(p => p.file_name == dest_name)) { show_decision("Replace Existing Publication?", @"There is already a published publication with the name \"$dest_name\", if you continue it will be replaced.", true, () => close(), () => select_publish_identity(true)); } else { select_publish_identity(false); } } private string unpublish_file; public async void unpublish_from(CollectionConfig config, string file) { title = @"Unpublish from $(config.name)"; this.config = config; this.unpublish_file = file; pulse_loader(); loader_status.set_text(@"Looking up $(config.domain)…"); try{ client = yield window.collection_service.get_client(config); } catch(Error e) { show_error("Server not found", @"Could not find PPRF server details for $(config.domain): $(e.message)"); return; } cid = new ByteComposition.from_base64(config.collection_id); loader_status.set_text(@"Querying $(config.domain) for collection information…"); try { collection = yield do_in_bg(() => client.get_collection(cid)); } catch(Error e) { show_error("Could not download collection information", @"Failed to download collection information from server: $(e.message)"); return; } if(collection.publications.no(p => p.file_name == file)) { show_error("Publication not found", @"The publication does not appear in the latest collection data from the server"); return; } select_identity("Unpublish", () => complete_unpublish.begin()); } private async void complete_unpublish() { header_bar.show_end_title_buttons = false; stack.visible_child = loader; loader_status.set_text(@"Unpublishing \"$(unpublish_file)\"…"); pulse_loader(); try { yield do_void_in_bg(() => client.unpublish(cid, unpublish_file, identity_list.selected_identity)); } catch(Error e) { show_error("Unpublication failed", @"Failed to unpbulish \"$(unpublish_file)\": $(e.message)"); return; } loader_status.set_text(@"Rebuilding index…"); try { yield do_void_in_bg(() => client.rebuild_index(cid, identity_list.selected_identity)); } catch(Error e) { show_error("Index rebuild failed", @"The publication has been unpublished from the server, however there was an error rebuilding the collection's index: $(e.message)"); return; } status_page.title = "Publication Removed!"; stack.visible_child = complete; } private void select_publish_identity(bool unpub) { if(unpub) { old_publish_timestamp = collection.publications.first_or_default(p => p.file_name == dest_name)?.publication_time; } unpublish = unpub; select_identity("Publish", () => publish_with.begin()); } private void select_identity(string action_label, DecisionCallback continue_cb) { continue_action = continue_cb; stack.visible_child = identity_select; publish_button.label = action_label; identity_header.label = @"Select identity to $(action_label.down()) with"; try { identity_list.populate_identities(collection); } catch(Error e) { show_error("Error loading identity list", @"There was an error retreiving a list of local identities: $(e.message)"); return; } publish_button.sensitive = identity_list.has_entries; header_bar.show_end_title_buttons = true; } private async void publish_with() { header_bar.show_end_title_buttons = false; stack.visible_child = loader; loader_status.set_text(@"Registering name \"$(dest_name)\"…"); pulse_loader(); try { yield do_void_in_bg(() => client.register_name(cid, dest_name, identity_list.selected_identity)); } catch(Pprf.Messages.PprfFailureError.NAME_EXISTS e) { // Ignore if unpublishing anyway if(unpublish) { yield upload_and_publish(true); return; } else{ show_decision("Overwrite file?", @"There is already a file on this server with the name \"$dest_name\", if you continue it will be overwritten.", true, () => close(), () => upload_and_publish.begin(true)); return; } } catch(Error e) { show_error("Name registration failed", @"Could not register the name \"$(dest_name)\" with the server: $(e.message)"); return; } yield upload_and_publish(false); } private async void upload_and_publish(bool replace_destination) { stack.visible_child = loader; loader_status.set_text(@"Preparing to upload publication…"); pulse_loader(); try { var file_info = yield publication_file.query_info_async("*", FileQueryInfoFlags.NONE, 1); var file_size = file_info.get_size(); var file_stream = yield publication_file.read_async(1); var flags = replace_destination ? Pprf.Messages.FinaliseUploadFlags.OVERWRITE_DESTINATION : 0; yield do_void_in_bg(() => client.upload(cid, file_stream, file_size, dest_name, unpublish, identity_list.selected_identity, upload_callback, flags)); } catch(Error e) { show_error("Upload failed", @"There was an error uploading the publication to the server: $(e.message)"); return; } pulse_loader(); loader_status.set_text(@"Computing publication checksum…"); try { var checksum = yield do_in_bg(() => new ByteComposition.from_byte_array(Pprf.Util.file_checksum(publication_file))); loader_status.set_text(@"Signing publication…"); var timestamp = old_publish_timestamp; if(timestamp == null) { timestamp = new DateTime.now_local(); } var publication = yield do_in_bg(() => new Ppub.CollectionPublication(dest_name, timestamp, identity_list.selected_identity.name, identity_list.selected_credentials, checksum.to_array())); loader_status.set_text(@"Publishing…"); yield do_void_in_bg(() => client.publish(cid, publication, identity_list.selected_identity)); } catch(Error e) { show_error("Publication failed", @"There was an error publishing the publication: $(e.message)"); return; } loader_status.set_text(@"Rebuilding index…"); try { yield do_void_in_bg(() => client.rebuild_index(cid, identity_list.selected_identity)); } catch(Error e) { show_error("Index rebuild failed", @"The publication has been uploaded to the server, however there was an error rebuilding the collection's index: $(e.message)"); return; } stack.visible_child = complete; loader_pulsing = false; } private double upload_frac; private void upload_callback(uint64 bytes_sent, uint64 bytes_total, Pprf.UploadStatus status) { print(@"Got update: $status\n"); upload_frac = ((double)bytes_sent)/((double)bytes_total); switch(status) { case Pprf.UploadStatus.INITIATING_SESSION: Idle.add_once(() => loader_status.set_text(@"Establishing upload session with server…")); break; case Pprf.UploadStatus.UPLOADING_CHUNK: case Pprf.UploadStatus.UPLOADED_CHUNK: Idle.add_once(() => { loader_pulsing = false; loader_status.set_text(@"Uploading publication…"); loader_progress.fraction = upload_frac; }); break; case Pprf.UploadStatus.UNPUBLISHING: Idle.add_once(() => { loader_status.set_text(@"Unpublishing previous publication…"); pulse_loader(); }); break; case Pprf.UploadStatus.FINALISING_SESSION: Idle.add_once(() => { loader_status.set_text(@"Finalising upload…"); pulse_loader(); }); break; case Pprf.UploadStatus.COMPLETE: Idle.add_once(() => { loader_status.set_text(@"Upload completed!"); pulse_loader(); }); break; } } } }