1/* 2* Copyright (c) 2017-2020 Alecaddd (https://alecaddd.com) 3* 4* This program is free software; you can redistribute it and/or 5* modify it under the terms of the GNU General Public 6* License as published by the Free Software Foundation; either 7* version 2 of the License, or (at your option) any later version. 8* 9* This program is distributed in the hope that it will be useful, 10* but WITHOUT ANY WARRANTY; without even the implied warranty of 11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12* General Public License for more details. 13* 14* You should have received a copy of the GNU General Public 15* License along with this program; if not, write to the 16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 17* Boston, MA 02110-1301 USA 18* 19* Authored by: Alessandro "Alecaddd" Castellani <castellani.ale@gmail.com> 20*/ 21 22public class Sequeler.Layouts.Library : Gtk.Grid { 23 public weak Sequeler.Window window { get; construct; } 24 25 GLib.File? file; 26 Gtk.TextBuffer buffer; 27 28 private Gtk.Grid title; 29 private Gtk.Revealer motion_revealer; 30 public Gtk.ListBox item_box; 31 public Gtk.ScrolledWindow scroll; 32 public Sequeler.Partials.HeaderBarButton delete_all; 33 34 public Gee.HashMap<string, string> real_data; 35 public Gtk.Spinner real_spinner; 36 public Gtk.ModelButton real_button; 37 public Sequeler.Services.ConnectionManager connection_manager; 38 39 public signal void edit_dialog (Gee.HashMap data); 40 41 // Datatype restrictions on DnD (Gtk.TargetFlags). 42 public const Gtk.TargetEntry[] TARGET_ENTRIES_LABEL = { 43 { "LIBRARYITEM", Gtk.TargetFlags.SAME_APP, 0 } 44 }; 45 46 public Library (Sequeler.Window main_window) { 47 Object ( 48 orientation: Gtk.Orientation.VERTICAL, 49 window: main_window, 50 width_request: 260, 51 column_homogeneous: true 52 ); 53 } 54 55 construct { 56 var motion_grid = new Gtk.Grid (); 57 motion_grid.margin = 6; 58 motion_grid.get_style_context ().add_class ("grid-motion"); 59 motion_grid.height_request = 18; 60 61 motion_revealer = new Gtk.Revealer (); 62 motion_revealer.transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN; 63 motion_revealer.add (motion_grid); 64 65 var titlebar = new Sequeler.Partials.TitleBar (_("SAVED CONNECTIONS")); 66 67 title = new Gtk.Grid (); 68 title.attach (titlebar, 0, 0); 69 title.attach (motion_revealer, 0, 1); 70 71 var toolbar = new Gtk.Grid (); 72 toolbar.get_style_context ().add_class ("library-toolbar"); 73 74 delete_all = new Sequeler.Partials.HeaderBarButton ("user-trash-symbolic", _("Delete All")); 75 delete_all.halign = Gtk.Align.END; 76 delete_all.hexpand = true; 77 delete_all.clicked.connect (() => { 78 confirm_delete_all (); 79 }); 80 81 var reload_btn = new Sequeler.Partials.HeaderBarButton ("view-refresh-symbolic", _("Reload Library")); 82 reload_btn.clicked.connect (() => reload_library.begin ()); 83 84 var export_btn = new Sequeler.Partials.HeaderBarButton ("document-save-symbolic", _("Export Library")); 85 export_btn.clicked.connect (export_library); 86 87 toolbar.attach (reload_btn, 0, 0, 1, 1); 88 toolbar.attach (new Gtk.Separator (Gtk.Orientation.VERTICAL), 1, 0, 1, 1); 89 toolbar.attach (export_btn, 2, 0, 1, 1); 90 toolbar.attach (new Gtk.Separator (Gtk.Orientation.VERTICAL), 3, 0, 1, 1); 91 toolbar.attach (delete_all, 4, 0, 1, 1); 92 93 scroll = new Gtk.ScrolledWindow (null, null); 94 scroll.hscrollbar_policy = Gtk.PolicyType.AUTOMATIC; 95 scroll.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC; 96 97 item_box = new Gtk.ListBox (); 98 item_box.get_style_context ().add_class ("library-box"); 99 item_box.set_activate_on_single_click (false); 100 item_box.selection_mode = Gtk.SelectionMode.SINGLE; 101 item_box.valign = Gtk.Align.FILL; 102 item_box.expand = true; 103 104 scroll.add (item_box); 105 106 foreach (var conn in settings.saved_connections) { 107 add_item (settings.arraify_data (conn)); 108 } 109 110 if (settings.saved_connections.length > 0) { 111 delete_all.sensitive = true; 112 } 113 114 item_box.row_activated.connect ((row) => { 115 var item = row as Sequeler.Partials.LibraryItem; 116 item.spinner.start (); 117 item.connect_button.sensitive = false; 118 window.data_manager.data = item.data; 119 init_connection_begin (item.data, item.spinner, item.connect_button, false); 120 }); 121 122 attach (title, 0, 0, 1, 1); 123 scroll.expand = true; 124 attach (scroll, 0, 1, 1, 2); 125 attach (toolbar, 0, 3, 1, 1); 126 127 build_drag_and_drop (); 128 } 129 130 private void build_drag_and_drop () { 131 Gtk.drag_dest_set (item_box, Gtk.DestDefaults.ALL, TARGET_ENTRIES_LABEL, Gdk.DragAction.MOVE); 132 item_box.drag_data_received.connect (on_drag_data_received); 133 134 Gtk.drag_dest_set (title, Gtk.DestDefaults.ALL, TARGET_ENTRIES_LABEL, Gdk.DragAction.MOVE); 135 title.drag_data_received.connect (on_drag_item_received); 136 title.drag_motion.connect (on_drag_motion); 137 title.drag_leave.connect (on_drag_leave); 138 } 139 140 private void on_drag_data_received (Gdk.DragContext context, int x, int y, 141 Gtk.SelectionData selection_data, uint target_type, uint time) { 142 int new_pos; 143 var target = (Partials.LibraryItem) item_box.get_row_at_y (y); 144 145 var row = ((Gtk.Widget[]) selection_data.get_data ())[0]; 146 var source = (Partials.LibraryItem) row; 147 148 int last_index = (int) item_box.get_children ().length (); 149 150 if (target == null) { 151 new_pos = last_index - 1; 152 } else { 153 new_pos = source.get_index () < target.get_index () 154 ? target.get_index () 155 : target.get_index () + 1; 156 } 157 158 settings.reorder_connection (source.data, new_pos); 159 reload_library.begin (); 160 } 161 162 private void on_drag_item_received (Gdk.DragContext context, int x, int y, 163 Gtk.SelectionData selection_data, uint target_type, uint time) { 164 var row = ((Gtk.Widget[]) selection_data.get_data ())[0]; 165 var source = (Partials.LibraryItem) row; 166 167 settings.reorder_connection (source.data, 0); 168 reload_library.begin (); 169 } 170 171 public bool on_drag_motion (Gdk.DragContext context, int x, int y, uint time) { 172 motion_revealer.reveal_child = true; 173 return true; 174 } 175 176 public void on_drag_leave (Gdk.DragContext context, uint time) { 177 motion_revealer.reveal_child = false; 178 } 179 180 public void add_item (Gee.HashMap<string, string> data) { 181 var item = new Sequeler.Partials.LibraryItem (data); 182 item.scrolled = scroll; 183 item_box.add (item); 184 185 item.confirm_delete.connect ((item, data) => { 186 confirm_delete (item, data); 187 }); 188 189 item.edit_dialog.connect ((data) => { 190 window.data_manager.data = data; 191 192 if (window.connection_dialog == null) { 193 window.connection_dialog = new Sequeler.Widgets.ConnectionDialog (window); 194 window.connection_dialog.show_all (); 195 196 window.connection_dialog.destroy.connect (() => { 197 window.connection_dialog = null; 198 }); 199 } 200 201 window.connection_dialog.present (); 202 }); 203 204 item.duplicate_connection.connect ((data) => { 205 duplicate_connection.begin (data); 206 }); 207 208 item.connect_to.connect ((data, spinner, connect_button) => { 209 window.data_manager.data = data; 210 init_connection_begin (data, spinner, connect_button); 211 }); 212 } 213 214 public void confirm_delete (Gtk.ListBoxRow item, Gee.HashMap<string, string> data) { 215 var message_dialog = new Granite.MessageDialog.with_image_from_icon_name (_("Are you sure you want to proceed?"), _("By deleting this connection you won’t be able to recover this data."), "dialog-warning", Gtk.ButtonsType.CANCEL); 216 message_dialog.transient_for = window; 217 218 var suggested_button = new Gtk.Button.with_label (_("Yes, Delete!")); 219 suggested_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); 220 message_dialog.add_action_widget (suggested_button, Gtk.ResponseType.ACCEPT); 221 222 message_dialog.show_all (); 223 if (message_dialog.run () == Gtk.ResponseType.ACCEPT) { 224 settings.delete_connection (data); 225 item_box.remove (item); 226 reload_library.begin (); 227 } 228 229 message_dialog.destroy (); 230 } 231 232 public void confirm_delete_all () { 233 var message_dialog = new Granite.MessageDialog.with_image_from_icon_name (_("Are you sure you want to proceed?"), _("All the data will be deleted and you won’t be able to recover it."), "dialog-warning", Gtk.ButtonsType.CANCEL); 234 message_dialog.transient_for = window; 235 236 var suggested_button = new Gtk.Button.with_label (_("Yes, Delete All!")); 237 suggested_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); 238 message_dialog.add_action_widget (suggested_button, Gtk.ResponseType.ACCEPT); 239 240 message_dialog.show_all (); 241 if (message_dialog.run () == Gtk.ResponseType.ACCEPT) { 242 settings.clear_connections (); 243 item_box.forall ((item) => item_box.remove (item)); 244 reload_library.begin (); 245 } 246 247 message_dialog.destroy (); 248 } 249 250 public async void reload_library () { 251 item_box.@foreach ((item) => item_box.remove (item)); 252 253 foreach (var new_conn in settings.saved_connections) { 254 var array = settings.arraify_data (new_conn); 255 add_item (array); 256 } 257 item_box.show_all (); 258 259 delete_all.sensitive = (settings.saved_connections.length > 0); 260 } 261 262 public async void check_add_item (Gee.HashMap<string, string> data) { 263 bool result = false; 264 265 SourceFunc callback = check_add_item.callback; 266 new Thread<void*> ("check-add-item", () => { 267 result = update_existing_connection (data); 268 269 Idle.add ((owned) callback); 270 Thread.exit (null); 271 272 return null; 273 }); 274 275 yield; 276 277 if (!result) { 278 settings.add_connection (data); 279 } 280 281 yield reload_library (); 282 } 283 284 private bool update_existing_connection (Gee.HashMap<string, string> data) { 285 foreach (var conn in settings.saved_connections) { 286 var check = settings.arraify_data (conn); 287 288 if (check["id"] == data["id"]) { 289 settings.edit_connection (data, conn); 290 return true; 291 } 292 } 293 294 return false; 295 } 296 297 public void check_open_sqlite_file (string path, string name) { 298 foreach (var conn in settings.saved_connections) { 299 var check = settings.arraify_data (conn); 300 if (check["file_path"] == path) { 301 settings.edit_connection (check, conn); 302 reload_library.begin ((obj, res) => { 303 item_box.get_row_at_index (0).activate (); 304 }); 305 return; 306 } 307 } 308 309 var data = new Gee.HashMap<string, string> (); 310 311 data.set ("id", settings.tot_connections.to_string ()); 312 data.set ("title", name); 313 data.set ("color", "rgb(222,222,222)"); 314 data.set ("type", "SQLite"); 315 data.set ("host", ""); 316 data.set ("name", ""); 317 data.set ("file_path", path); 318 data.set ("username", ""); 319 data.set ("password", ""); 320 data.set ("port", ""); 321 322 settings.add_connection (data); 323 324 reload_library.begin ((obj, res) => { 325 item_box.get_row_at_index (0).activate (); 326 }); 327 } 328 329 private void init_connection_begin (Gee.HashMap<string, string> data, Gtk.Spinner spinner, Gtk.ModelButton button, bool update = true) { 330 connection_manager = new Sequeler.Services.ConnectionManager (window, data); 331 332 if (data["type"] != "SQLite" && data["username"] == "") { 333 spinner.stop (); 334 button.sensitive = true; 335 connection_warning (_("A username is required in order to connect!"), data["name"]); 336 return; 337 } 338 339 if (data["has_ssh"] == "true") { 340 real_data = data; 341 real_spinner = spinner; 342 real_button = button; 343 connection_manager.ssh_tunnel_ready.connect (() => 344 init_real_connection_begin (real_data, real_spinner, real_button, update) 345 ); 346 347 new Thread<void*> (null, () => { 348 var result = new Gee.HashMap<string, string> (); 349 try { 350 connection_manager.ssh_tunnel_init (true); 351 } catch (Error e) { 352 result["status"] = "false"; 353 result["message"] = e.message; 354 } 355 356 Idle.add (() => { 357 if (result["status"] == "false") { 358 spinner.stop (); 359 button.sensitive = true; 360 connection_warning (result["message"], data["name"]); 361 } 362 return false; 363 }); 364 365 return null; 366 }); 367 } else { 368 init_real_connection_begin (data, spinner, button, update); 369 } 370 } 371 372 private void init_real_connection_begin (Gee.HashMap<string, string> data, Gtk.Spinner spinner, Gtk.ModelButton button, bool update) { 373 var result = new Gee.HashMap<string, string> (); 374 375 connection_manager.init_connection.begin ((obj, res) => { 376 new Thread<void*> (null, () => { 377 try { 378 result = connection_manager.init_connection.end (res); 379 } catch (ThreadError e) { 380 connection_warning (e.message, data["name"]); 381 spinner.stop (); 382 button.sensitive = true; 383 } 384 385 Idle.add (() => { 386 spinner.stop (); 387 button.sensitive = true; 388 389 if (result["status"] == "true") { 390 if (settings.save_quick && update) { 391 check_add_item.begin (data); 392 } 393 394 window.main.connection_opened.begin (connection_manager); 395 } else { 396 connection_warning (result["msg"], data["name"]); 397 } 398 return false; 399 }); 400 return null; 401 }); 402 }); 403 } 404 405 private void export_library () { 406 file = null; 407 buffer = new Gtk.TextBuffer (null); 408 409 var save_dialog = new Gtk.FileChooserNative (_("Pick a file"), 410 window, 411 Gtk.FileChooserAction.SAVE, 412 _("_Save"), 413 _("_Cancel")); 414 415 save_dialog.do_overwrite_confirmation = true; 416 save_dialog.modal = true; 417 save_dialog.response.connect ((dialog, response_id) => { 418 switch (response_id) { 419 case Gtk.ResponseType.ACCEPT: 420 file = save_dialog.get_file (); 421 save_to_file.begin (); 422 break; 423 default: 424 break; 425 } 426 dialog.destroy (); 427 }); 428 429 save_dialog.run (); 430 } 431 432 private async void save_to_file () { 433 var buffer_content = ""; 434 var library = settings.saved_connections; 435 436 foreach (var lib in library) { 437 var array = settings.arraify_data (lib); 438 439 try { 440 array["password"] = yield password_mngr.get_password_async (array["id"]); 441 } catch (Error e) { 442 debug ("Unable to get the password from libsecret"); 443 } 444 445 if (array["has_ssh"] == "true") { 446 try { 447 array["ssh_password"] = yield password_mngr.get_password_async (array["id"] + "9999"); 448 } catch { 449 debug ("Unable to get the SSH password from libsecret"); 450 } 451 } 452 453 buffer_content += settings.stringify_data (array) + "---\n"; 454 } 455 456 buffer.set_text (buffer_content); 457 458 Gtk.TextIter start; 459 Gtk.TextIter end; 460 461 buffer.get_bounds (out start, out end); 462 string current_contents = buffer.get_text (start, end, false); 463 try { 464 file.replace_contents (current_contents.data, null, false, GLib.FileCreateFlags.NONE, null, null); 465 } 466 catch (GLib.Error err) { 467 export_warning (err.message); 468 } 469 } 470 471 private void connection_warning (string message, string title) { 472 var message_dialog = new Granite.MessageDialog.with_image_from_icon_name (_("Unable to Connect to %s").printf (title), message, "dialog-error", Gtk.ButtonsType.NONE); 473 message_dialog.transient_for = window; 474 475 var suggested_button = new Gtk.Button.with_label ("Close"); 476 message_dialog.add_action_widget (suggested_button, Gtk.ResponseType.ACCEPT); 477 478 message_dialog.show_all (); 479 if (message_dialog.run () == Gtk.ResponseType.ACCEPT) {} 480 481 message_dialog.destroy (); 482 } 483 484 private void export_warning (string message) { 485 var message_dialog = new Granite.MessageDialog.with_image_from_icon_name (_("Unable to Export Library "), message, "dialog-error", Gtk.ButtonsType.NONE); 486 message_dialog.transient_for = window; 487 488 var suggested_button = new Gtk.Button.with_label ("Close"); 489 message_dialog.add_action_widget (suggested_button, Gtk.ResponseType.ACCEPT); 490 491 message_dialog.show_all (); 492 if (message_dialog.run () == Gtk.ResponseType.ACCEPT) {} 493 494 message_dialog.destroy (); 495 } 496 497 private async void duplicate_connection (Gee.HashMap<string, string> data) { 498 if (data["type"] != "SQLite") { 499 try { 500 data["password"] = yield password_mngr.get_password_async (data["id"]); 501 } catch (Error e) { 502 debug ("Unable to get the password from libsecret"); 503 } 504 } 505 506 if (data["has_ssh"] == "true") { 507 try { 508 data["ssh_password"] = yield password_mngr.get_password_async (data["id"] + "9999"); 509 } catch { 510 debug ("Unable to get the SSH password from libsecret"); 511 } 512 } 513 514 yield settings.duplicate_connection (data); 515 yield reload_library (); 516 } 517} 518