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