PublishWindow.vala 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. using Gtk;
  2. using Adw;
  3. using Invercargill;
  4. namespace Publicate {
  5. public class PublishWindow : Adw.Window {
  6. private Adw.HeaderBar header_bar;
  7. private ViewerWindow window;
  8. private Stack stack;
  9. private Box loader;
  10. private Label loader_status;
  11. private ProgressBar loader_progress;
  12. private bool loader_pulsing = false;
  13. private Box identity_select;
  14. private Label identity_header;
  15. private IdentityList identity_list;
  16. private Button publish_button;
  17. private Box decision;
  18. private Label decision_heading;
  19. private Label decision_body;
  20. private Button cancel_button;
  21. private Button continue_button;
  22. private Box error_view;
  23. private Label error_heading;
  24. private Label error_body;
  25. private Box complete;
  26. private StatusPage status_page;
  27. private delegate void DecisionCallback();
  28. private DecisionCallback cancel_action;
  29. private DecisionCallback continue_action;
  30. public PublishWindow(ViewerWindow window) {
  31. this.window = window;
  32. modal = true;
  33. transient_for = window;
  34. var box = new Box(Orientation.VERTICAL, 0);
  35. header_bar = new Adw.HeaderBar();
  36. header_bar.show_end_title_buttons = false;
  37. title = "Publish";
  38. default_height = 500;
  39. default_width = 700;
  40. box.append (header_bar);
  41. stack = new Stack();
  42. stack.vexpand = true;
  43. stack.transition_type = StackTransitionType.CROSSFADE;
  44. box.append(stack);
  45. content = box;
  46. loader = new Box(Orientation.VERTICAL, 8);
  47. loader_status = new Label ("Please wait…");
  48. loader_status.halign = Align.START;
  49. loader_progress = new ProgressBar ();
  50. loader_progress.hexpand = true;
  51. loader.append (loader_status);
  52. loader.append (loader_progress);
  53. loader.width_request = 550;
  54. loader.valign = Align.CENTER;
  55. loader.halign = Align.CENTER;
  56. stack.add_child (loader);
  57. stack.visible_child = loader;
  58. identity_select = new Box(Orientation.VERTICAL, 8);
  59. identity_select.margin_top = 18;
  60. identity_select.margin_start = 18;
  61. identity_select.margin_end = 18;
  62. identity_select.margin_bottom = 18;
  63. identity_header = new Label("Select identity to publish with");
  64. identity_header.halign = Align.START;
  65. identity_header.add_css_class("title-2");
  66. identity_select.append(identity_header);
  67. identity_list = new IdentityList(window);
  68. identity_select.append(identity_list);
  69. publish_button = new Button.with_label("Publish");
  70. publish_button.add_css_class("suggested-action");
  71. publish_button.halign = Align.END;
  72. publish_button.clicked.connect(() => continue_action());
  73. identity_select.append(publish_button);
  74. stack.add_child(identity_select);
  75. error_view = new Box(Orientation.VERTICAL, 8);
  76. error_view.halign = Align.CENTER;
  77. error_view.valign = Align.CENTER;
  78. error_view.margin_top = 18;
  79. error_view.margin_start = 64;
  80. error_view.margin_end = 64;
  81. error_view.margin_bottom = 18;
  82. error_heading.width_request = 200;
  83. error_heading = new Label("Decision");
  84. error_heading.halign = Align.START;
  85. error_heading.add_css_class("title-2");
  86. error_view.append(error_heading);
  87. error_body = new Label("Details");
  88. error_body.halign = Align.START;
  89. error_body.hexpand = true;
  90. error_body.wrap = true;
  91. error_body.wrap_mode = Pango.WrapMode.WORD;
  92. error_body.natural_wrap_mode = NaturalWrapMode.WORD;
  93. error_view.append(error_body);
  94. var error_close_button = new Button.with_label("Close");
  95. error_close_button.hexpand = true;
  96. error_close_button.clicked.connect(() => close());
  97. error_view.append(error_close_button);
  98. stack.add_child(error_view);
  99. decision = new Box(Orientation.VERTICAL, 8);
  100. decision.halign = Align.CENTER;
  101. decision.valign = Align.CENTER;
  102. decision.margin_top = 18;
  103. decision.margin_start = 64;
  104. decision.margin_end = 64;
  105. decision.margin_bottom = 18;
  106. decision.width_request = 200;
  107. decision_heading = new Label("Decision");
  108. decision_heading.halign = Align.START;
  109. decision_heading.add_css_class("title-2");
  110. decision.append(decision_heading);
  111. decision_body = new Label("Details");
  112. decision_body.halign = Align.START;
  113. decision_body.hexpand = true;
  114. decision_body.wrap = true;
  115. decision_body.wrap_mode = Pango.WrapMode.WORD;
  116. decision_body.natural_wrap_mode = NaturalWrapMode.WORD;
  117. decision.append(decision_body);
  118. var button_box = new Box(Orientation.HORIZONTAL, 0);
  119. button_box.add_css_class("linked");
  120. cancel_button = new Button.with_label("Cancel");
  121. cancel_button.hexpand = true;
  122. cancel_button.clicked.connect(() => cancel_action());
  123. button_box.append(cancel_button);
  124. continue_button = new Button.with_label("Continue");
  125. continue_button.hexpand = true;
  126. continue_button.clicked.connect(() => continue_action());
  127. button_box.append(continue_button);
  128. decision.append(button_box);
  129. stack.add_child(decision);
  130. complete = new Box(Orientation.VERTICAL, 8);
  131. complete.halign = Align.CENTER;
  132. complete.valign = Align.CENTER;
  133. complete.margin_top = 18;
  134. complete.margin_start = 18;
  135. complete.margin_end = 18;
  136. complete.margin_bottom = 18;
  137. status_page = new StatusPage();
  138. complete.append(status_page);
  139. status_page.icon_name = "emblem-ok-symbolic";
  140. status_page.title = "Publish Completed!";
  141. var close_button = new Button.with_label("Close");
  142. status_page.child = close_button;
  143. close_button.width_request = 400;
  144. close_button.clicked.connect(() => close());
  145. stack.add_child(complete);
  146. }
  147. private void pulse_loader() {
  148. if(loader_pulsing) {
  149. return;
  150. }
  151. loader_pulsing = true;
  152. Timeout.add(100, () => {
  153. if(!loader_pulsing) {
  154. return false;
  155. }
  156. loader_progress.pulse();
  157. return true;
  158. });
  159. }
  160. private void show_decision(string heading, string body, bool is_destructive, DecisionCallback cancel_cb, DecisionCallback continue_cb) {
  161. decision_heading.set_text(heading);
  162. decision_body.set_text(body);
  163. continue_button.set_css_classes(new string[] { is_destructive ? "destructive-action" : "suggested-action"});
  164. continue_action = continue_cb;
  165. cancel_action = cancel_cb;
  166. stack.visible_child = decision;
  167. }
  168. private void show_error(string heading, string body) {
  169. error_heading.set_text(heading);
  170. error_body.set_text(body);
  171. stack.visible_child = error_view;
  172. error_bell();
  173. }
  174. private Ppub.Collection collection;
  175. private CollectionConfig config;
  176. private Pprf.Client client;
  177. private File publication_file;
  178. private string dest_name;
  179. private BinaryData cid;
  180. private bool unpublish = false;
  181. private DateTime? old_publish_timestamp = null;
  182. public async void publish_to(CollectionConfig config, File publication_file) {
  183. title = @"Publish to $(config.name)";
  184. this.config = config;
  185. this.publication_file = publication_file;
  186. dest_name = publication_file.get_basename();
  187. if(!dest_name.has_suffix(".ppub")) {
  188. dest_name += ".ppub";
  189. }
  190. pulse_loader();
  191. loader_status.set_text(@"Looking up $(config.domain)…");
  192. try{
  193. client = yield window.collection_service.get_client(config);
  194. }
  195. catch(Error e) {
  196. show_error("Server not found", @"Could not find PPRF server details for $(config.domain): $(e.message)");
  197. return;
  198. }
  199. cid = new ByteComposition.from_base64(config.collection_id);
  200. loader_status.set_text(@"Querying $(config.domain) for collection information…");
  201. try {
  202. collection = yield do_in_bg<Ppub.Collection>(() => client.get_collection(cid));
  203. }
  204. catch(Error e) {
  205. show_error("Could not download collection information", @"Failed to download collection information from server: $(e.message)");
  206. return;
  207. }
  208. loader_pulsing = false;
  209. loader_progress.fraction = 0;
  210. if(collection.publications.any(p => p.file_name == dest_name)) {
  211. 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));
  212. }
  213. else {
  214. select_publish_identity(false);
  215. }
  216. }
  217. private string unpublish_file;
  218. public async void unpublish_from(CollectionConfig config, string file) {
  219. title = @"Unpublish from $(config.name)";
  220. this.config = config;
  221. this.unpublish_file = file;
  222. pulse_loader();
  223. loader_status.set_text(@"Looking up $(config.domain)…");
  224. try{
  225. client = yield window.collection_service.get_client(config);
  226. }
  227. catch(Error e) {
  228. show_error("Server not found", @"Could not find PPRF server details for $(config.domain): $(e.message)");
  229. return;
  230. }
  231. cid = new ByteComposition.from_base64(config.collection_id);
  232. loader_status.set_text(@"Querying $(config.domain) for collection information…");
  233. try {
  234. collection = yield do_in_bg<Ppub.Collection>(() => client.get_collection(cid));
  235. }
  236. catch(Error e) {
  237. show_error("Could not download collection information", @"Failed to download collection information from server: $(e.message)");
  238. return;
  239. }
  240. if(collection.publications.no(p => p.file_name == file)) {
  241. show_error("Publication not found", @"The publication does not appear in the latest collection data from the server");
  242. return;
  243. }
  244. select_identity("Unpublish", () => complete_unpublish.begin());
  245. }
  246. private async void complete_unpublish() {
  247. header_bar.show_end_title_buttons = false;
  248. stack.visible_child = loader;
  249. loader_status.set_text(@"Unpublishing \"$(unpublish_file)\"…");
  250. pulse_loader();
  251. try {
  252. yield do_void_in_bg(() => client.unpublish(cid, unpublish_file, identity_list.selected_identity));
  253. }
  254. catch(Error e) {
  255. show_error("Unpublication failed", @"Failed to unpbulish \"$(unpublish_file)\": $(e.message)");
  256. return;
  257. }
  258. loader_status.set_text(@"Rebuilding index…");
  259. try {
  260. yield do_void_in_bg(() => client.rebuild_index(cid, identity_list.selected_identity));
  261. }
  262. catch(Error e) {
  263. 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)");
  264. return;
  265. }
  266. status_page.title = "Publication Removed!";
  267. stack.visible_child = complete;
  268. }
  269. private void select_publish_identity(bool unpub) {
  270. if(unpub) {
  271. old_publish_timestamp = collection.publications.first_or_default(p => p.file_name == dest_name)?.publication_time;
  272. }
  273. unpublish = unpub;
  274. select_identity("Publish", () => publish_with.begin());
  275. }
  276. private void select_identity(string action_label, DecisionCallback continue_cb) {
  277. continue_action = continue_cb;
  278. stack.visible_child = identity_select;
  279. publish_button.label = action_label;
  280. identity_header.label = @"Select identity to $(action_label.down()) with";
  281. try {
  282. identity_list.populate_identities(collection);
  283. }
  284. catch(Error e) {
  285. show_error("Error loading identity list", @"There was an error retreiving a list of local identities: $(e.message)");
  286. return;
  287. }
  288. publish_button.sensitive = identity_list.has_entries;
  289. header_bar.show_end_title_buttons = true;
  290. }
  291. private async void publish_with() {
  292. header_bar.show_end_title_buttons = false;
  293. stack.visible_child = loader;
  294. loader_status.set_text(@"Registering name \"$(dest_name)\"…");
  295. pulse_loader();
  296. try {
  297. yield do_void_in_bg(() => client.register_name(cid, dest_name, identity_list.selected_identity));
  298. }
  299. catch(Pprf.Messages.PprfFailureError.NAME_EXISTS e) {
  300. // Ignore if unpublishing anyway
  301. if(unpublish) {
  302. yield upload_and_publish(true);
  303. return;
  304. }
  305. else{
  306. 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));
  307. return;
  308. }
  309. }
  310. catch(Error e) {
  311. show_error("Name registration failed", @"Could not register the name \"$(dest_name)\" with the server: $(e.message)");
  312. return;
  313. }
  314. yield upload_and_publish(false);
  315. }
  316. private async void upload_and_publish(bool replace_destination) {
  317. stack.visible_child = loader;
  318. loader_status.set_text(@"Preparing to upload publication…");
  319. pulse_loader();
  320. try {
  321. var file_info = yield publication_file.query_info_async("*", FileQueryInfoFlags.NONE, 1);
  322. var file_size = file_info.get_size();
  323. var file_stream = yield publication_file.read_async(1);
  324. var flags = replace_destination ? Pprf.Messages.FinaliseUploadFlags.OVERWRITE_DESTINATION : 0;
  325. yield do_void_in_bg(() => client.upload(cid, file_stream, file_size, dest_name, unpublish, identity_list.selected_identity, upload_callback, flags));
  326. }
  327. catch(Error e) {
  328. show_error("Upload failed", @"There was an error uploading the publication to the server: $(e.message)");
  329. return;
  330. }
  331. pulse_loader();
  332. loader_status.set_text(@"Computing publication checksum…");
  333. try {
  334. var checksum = yield do_in_bg<BinaryData>(() => new ByteComposition.from_byte_array(Pprf.Util.file_checksum(publication_file)));
  335. loader_status.set_text(@"Signing publication…");
  336. var timestamp = old_publish_timestamp;
  337. if(timestamp == null) {
  338. timestamp = new DateTime.now_local();
  339. }
  340. var publication = yield do_in_bg<Ppub.CollectionPublication>(() => new Ppub.CollectionPublication(dest_name, timestamp, identity_list.selected_identity.name, identity_list.selected_credentials, checksum.to_array()));
  341. loader_status.set_text(@"Publishing…");
  342. yield do_void_in_bg(() => client.publish(cid, publication, identity_list.selected_identity));
  343. }
  344. catch(Error e) {
  345. show_error("Publication failed", @"There was an error publishing the publication: $(e.message)");
  346. return;
  347. }
  348. loader_status.set_text(@"Rebuilding index…");
  349. try {
  350. yield do_void_in_bg(() => client.rebuild_index(cid, identity_list.selected_identity));
  351. }
  352. catch(Error e) {
  353. 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)");
  354. return;
  355. }
  356. stack.visible_child = complete;
  357. loader_pulsing = false;
  358. }
  359. private double upload_frac;
  360. private void upload_callback(uint64 bytes_sent, uint64 bytes_total, Pprf.UploadStatus status) {
  361. print(@"Got update: $status\n");
  362. upload_frac = ((double)bytes_sent)/((double)bytes_total);
  363. switch(status) {
  364. case Pprf.UploadStatus.INITIATING_SESSION:
  365. Idle.add_once(() => loader_status.set_text(@"Establishing upload session with server…"));
  366. break;
  367. case Pprf.UploadStatus.UPLOADING_CHUNK:
  368. case Pprf.UploadStatus.UPLOADED_CHUNK:
  369. Idle.add_once(() => {
  370. loader_pulsing = false;
  371. loader_status.set_text(@"Uploading publication…");
  372. loader_progress.fraction = upload_frac;
  373. });
  374. break;
  375. case Pprf.UploadStatus.UNPUBLISHING:
  376. Idle.add_once(() => {
  377. loader_status.set_text(@"Unpublishing previous publication…");
  378. pulse_loader();
  379. });
  380. break;
  381. case Pprf.UploadStatus.FINALISING_SESSION:
  382. Idle.add_once(() => {
  383. loader_status.set_text(@"Finalising upload…");
  384. pulse_loader();
  385. });
  386. break;
  387. case Pprf.UploadStatus.COMPLETE:
  388. Idle.add_once(() => {
  389. loader_status.set_text(@"Upload completed!");
  390. pulse_loader();
  391. });
  392. break;
  393. }
  394. }
  395. }
  396. }