1 /******************************************************************************
2  * Copyright (c) Transmission authors and contributors
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a
5  * copy of this software and associated documentation files (the "Software"),
6  * to deal in the Software without restriction, including without limitation
7  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8  * and/or sell copies of the Software, and to permit persons to whom the
9  * Software is furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in
12  * all copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20  * DEALINGS IN THE SOFTWARE.
21  *****************************************************************************/
22 
23 #include <locale.h>
24 #include <signal.h>
25 #include <string.h>
26 #include <stdio.h>
27 #include <stdlib.h> /* exit() */
28 #include <time.h>
29 
30 #include <glib/gi18n.h>
31 #include <glib/gstdio.h>
32 #include <gio/gio.h>
33 #include <gtk/gtk.h>
34 
35 #include <libtransmission/transmission.h>
36 #include <libtransmission/rpcimpl.h>
37 #include <libtransmission/utils.h>
38 #include <libtransmission/version.h>
39 
40 #include "actions.h"
41 #include "conf.h"
42 #include "details.h"
43 #include "dialogs.h"
44 #include "hig.h"
45 #include "makemeta-ui.h"
46 #include "msgwin.h"
47 #include "notify.h"
48 #include "open-dialog.h"
49 #include "relocate.h"
50 #include "stats.h"
51 #include "tr-core.h"
52 #include "tr-icon.h"
53 #include "tr-prefs.h"
54 #include "tr-window.h"
55 #include "util.h"
56 
57 #define MY_CONFIG_NAME "transmission"
58 #define MY_READABLE_NAME "transmission-gtk"
59 
60 #define SHOW_LICENSE
61 static char const* LICENSE =
62     "Copyright 2005-2020. All code is copyrighted by the respective authors.\n"
63     "\n"
64     "Transmission can be redistributed and/or modified under the terms of the "
65     "GNU GPL versions 2 or 3 or by any future license endorsed by Mnemosyne LLC.\n"
66     "\n"
67     "In addition, linking to and/or using OpenSSL is allowed.\n"
68     "\n"
69     "This program is distributed in the hope that it will be useful, "
70     "but WITHOUT ANY WARRANTY; without even the implied warranty of "
71     "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
72     "\n"
73     "Some of Transmission's source files have more permissive licenses. "
74     "Those files may, of course, be used on their own under their own terms.\n";
75 
76 struct cbdata
77 {
78     char* config_dir;
79     gboolean start_paused;
80     gboolean is_iconified;
81     gboolean is_closing;
82 
83     guint activation_count;
84     guint timer;
85     guint update_model_soon_tag;
86     guint refresh_actions_tag;
87     gpointer icon;
88     GtkWindow* wind;
89     TrCore* core;
90     GtkWidget* msgwin;
91     GtkWidget* prefs;
92     GSList* error_list;
93     GSList* duplicates_list;
94     GSList* details;
95     GtkTreeSelection* sel;
96 };
97 
gtr_window_present(GtkWindow * window)98 static void gtr_window_present(GtkWindow* window)
99 {
100     gtk_window_present_with_time(window, gtk_get_current_event_time());
101 }
102 
103 /***
104 ****
105 ****  DETAILS DIALOGS MANAGEMENT
106 ****
107 ***/
108 
compare_integers(gconstpointer a,gconstpointer b)109 static int compare_integers(gconstpointer a, gconstpointer b)
110 {
111     return GPOINTER_TO_INT(a) - GPOINTER_TO_INT(b);
112 }
113 
get_details_dialog_key(GSList * id_list)114 static char* get_details_dialog_key(GSList* id_list)
115 {
116     GSList* tmp = g_slist_sort(g_slist_copy(id_list), compare_integers);
117     GString* gstr = g_string_new(NULL);
118 
119     for (GSList* l = tmp; l != NULL; l = l->next)
120     {
121         g_string_append_printf(gstr, "%d ", GPOINTER_TO_INT(l->data));
122     }
123 
124     g_slist_free(tmp);
125     return g_string_free(gstr, FALSE);
126 }
127 
get_selected_torrent_ids_foreach(GtkTreeModel * model,GtkTreePath * p UNUSED,GtkTreeIter * iter,gpointer gdata)128 static void get_selected_torrent_ids_foreach(GtkTreeModel* model, GtkTreePath* p UNUSED, GtkTreeIter* iter, gpointer gdata)
129 {
130     int id;
131     GSList** ids = gdata;
132     gtk_tree_model_get(model, iter, MC_TORRENT_ID, &id, -1);
133     *ids = g_slist_append(*ids, GINT_TO_POINTER(id));
134 }
135 
get_selected_torrent_ids(struct cbdata * data)136 static GSList* get_selected_torrent_ids(struct cbdata* data)
137 {
138     GSList* ids = NULL;
139     gtk_tree_selection_selected_foreach(data->sel, get_selected_torrent_ids_foreach, &ids);
140     return ids;
141 }
142 
on_details_dialog_closed(gpointer gdata,GObject * dead)143 static void on_details_dialog_closed(gpointer gdata, GObject* dead)
144 {
145     struct cbdata* data = gdata;
146 
147     data->details = g_slist_remove(data->details, dead);
148 }
149 
show_details_dialog_for_selected_torrents(struct cbdata * data)150 static void show_details_dialog_for_selected_torrents(struct cbdata* data)
151 {
152     GtkWidget* dialog = NULL;
153     GSList* ids = get_selected_torrent_ids(data);
154     char* key = get_details_dialog_key(ids);
155 
156     for (GSList* l = data->details; dialog == NULL && l != NULL; l = l->next)
157     {
158         if (g_strcmp0(key, g_object_get_data(l->data, "key")) == 0)
159         {
160             dialog = l->data;
161         }
162     }
163 
164     if (dialog == NULL)
165     {
166         dialog = gtr_torrent_details_dialog_new(GTK_WINDOW(data->wind), data->core);
167         gtr_torrent_details_dialog_set_torrents(dialog, ids);
168         g_object_set_data_full(G_OBJECT(dialog), "key", g_strdup(key), g_free);
169         g_object_weak_ref(G_OBJECT(dialog), on_details_dialog_closed, data);
170         data->details = g_slist_append(data->details, dialog);
171         gtk_widget_show(dialog);
172     }
173 
174     gtr_window_present(GTK_WINDOW(dialog));
175     g_free(key);
176     g_slist_free(ids);
177 }
178 
179 /****
180 *****
181 *****  ON SELECTION CHANGED
182 *****
183 ****/
184 
185 struct counts_data
186 {
187     int total_count;
188     int queued_count;
189     int stopped_count;
190 };
191 
get_selected_torrent_counts_foreach(GtkTreeModel * model,GtkTreePath * path UNUSED,GtkTreeIter * iter,gpointer user_data)192 static void get_selected_torrent_counts_foreach(GtkTreeModel* model, GtkTreePath* path UNUSED, GtkTreeIter* iter,
193     gpointer user_data)
194 {
195     int activity = 0;
196     struct counts_data* counts = user_data;
197 
198     ++counts->total_count;
199 
200     gtk_tree_model_get(model, iter, MC_ACTIVITY, &activity, -1);
201 
202     if (activity == TR_STATUS_DOWNLOAD_WAIT || activity == TR_STATUS_SEED_WAIT)
203     {
204         ++counts->queued_count;
205     }
206 
207     if (activity == TR_STATUS_STOPPED)
208     {
209         ++counts->stopped_count;
210     }
211 }
212 
get_selected_torrent_counts(struct cbdata * data,struct counts_data * counts)213 static void get_selected_torrent_counts(struct cbdata* data, struct counts_data* counts)
214 {
215     counts->total_count = 0;
216     counts->queued_count = 0;
217     counts->stopped_count = 0;
218 
219     gtk_tree_selection_selected_foreach(data->sel, get_selected_torrent_counts_foreach, counts);
220 }
221 
count_updatable_foreach(GtkTreeModel * model,GtkTreePath * path UNUSED,GtkTreeIter * iter,gpointer accumulated_status)222 static void count_updatable_foreach(GtkTreeModel* model, GtkTreePath* path UNUSED, GtkTreeIter* iter,
223     gpointer accumulated_status)
224 {
225     tr_torrent* tor;
226     gtk_tree_model_get(model, iter, MC_TORRENT, &tor, -1);
227     *(int*)accumulated_status |= tr_torrentCanManualUpdate(tor);
228 }
229 
refresh_actions(gpointer gdata)230 static gboolean refresh_actions(gpointer gdata)
231 {
232     struct cbdata* data = gdata;
233 
234     if (!data->is_closing)
235     {
236         int canUpdate;
237         struct counts_data sel_counts;
238         size_t const total = gtr_core_get_torrent_count(data->core);
239         size_t const active = gtr_core_get_active_torrent_count(data->core);
240         int const torrent_count = gtk_tree_model_iter_n_children(gtr_core_model(data->core), NULL);
241         bool has_selection;
242 
243         get_selected_torrent_counts(data, &sel_counts);
244         has_selection = sel_counts.total_count > 0;
245 
246         gtr_action_set_sensitive("select-all", torrent_count != 0);
247         gtr_action_set_sensitive("deselect-all", torrent_count != 0);
248         gtr_action_set_sensitive("pause-all-torrents", active != 0);
249         gtr_action_set_sensitive("start-all-torrents", active != total);
250 
251         gtr_action_set_sensitive("torrent-stop", (sel_counts.stopped_count < sel_counts.total_count));
252         gtr_action_set_sensitive("torrent-start", (sel_counts.stopped_count) > 0);
253         gtr_action_set_sensitive("torrent-start-now", (sel_counts.stopped_count + sel_counts.queued_count) > 0);
254         gtr_action_set_sensitive("torrent-verify", has_selection);
255         gtr_action_set_sensitive("remove-torrent", has_selection);
256         gtr_action_set_sensitive("delete-torrent", has_selection);
257         gtr_action_set_sensitive("relocate-torrent", has_selection);
258         gtr_action_set_sensitive("queue-move-top", has_selection);
259         gtr_action_set_sensitive("queue-move-up", has_selection);
260         gtr_action_set_sensitive("queue-move-down", has_selection);
261         gtr_action_set_sensitive("queue-move-bottom", has_selection);
262         gtr_action_set_sensitive("show-torrent-properties", has_selection);
263         gtr_action_set_sensitive("open-torrent-folder", sel_counts.total_count == 1);
264         gtr_action_set_sensitive("copy-magnet-link-to-clipboard", sel_counts.total_count == 1);
265 
266         canUpdate = 0;
267         gtk_tree_selection_selected_foreach(data->sel, count_updatable_foreach, &canUpdate);
268         gtr_action_set_sensitive("torrent-reannounce", canUpdate != 0);
269     }
270 
271     data->refresh_actions_tag = 0;
272     return G_SOURCE_REMOVE;
273 }
274 
refresh_actions_soon(gpointer gdata)275 static void refresh_actions_soon(gpointer gdata)
276 {
277     struct cbdata* data = gdata;
278 
279     if (!data->is_closing && data->refresh_actions_tag == 0)
280     {
281         data->refresh_actions_tag = gdk_threads_add_idle(refresh_actions, data);
282     }
283 }
284 
on_selection_changed(GtkTreeSelection * s UNUSED,gpointer gdata)285 static void on_selection_changed(GtkTreeSelection* s UNUSED, gpointer gdata)
286 {
287     refresh_actions_soon(gdata);
288 }
289 
290 /***
291 ****
292 ***/
293 
has_magnet_link_handler(void)294 static gboolean has_magnet_link_handler(void)
295 {
296     GAppInfo* app_info = g_app_info_get_default_for_uri_scheme("magnet");
297     gboolean const has_handler = app_info != NULL;
298     g_clear_object(&app_info);
299     return has_handler;
300 }
301 
register_magnet_link_handler(void)302 static void register_magnet_link_handler(void)
303 {
304     GError* error;
305     GAppInfo* app;
306     char const* const content_type = "x-scheme-handler/magnet";
307 
308     error = NULL;
309     app = g_app_info_create_from_commandline("transmission-gtk", "transmission-gtk", G_APP_INFO_CREATE_SUPPORTS_URIS, &error);
310     g_app_info_set_as_default_for_type(app, content_type, &error);
311 
312     if (error != NULL)
313     {
314         g_warning(_("Error registering Transmission as a %s handler: %s"), content_type, error->message);
315         g_error_free(error);
316     }
317 
318     g_clear_object(&app);
319 }
320 
ensure_magnet_handler_exists(void)321 static void ensure_magnet_handler_exists(void)
322 {
323     if (!has_magnet_link_handler())
324     {
325         register_magnet_link_handler();
326     }
327 }
328 
on_main_window_size_allocated(GtkWidget * gtk_window,GtkAllocation * alloc UNUSED,gpointer gdata UNUSED)329 static void on_main_window_size_allocated(GtkWidget* gtk_window, GtkAllocation* alloc UNUSED, gpointer gdata UNUSED)
330 {
331     GdkWindow* gdk_window = gtk_widget_get_window(gtk_window);
332     gboolean const isMaximized = gdk_window != NULL && (gdk_window_get_state(gdk_window) & GDK_WINDOW_STATE_MAXIMIZED) != 0;
333 
334     gtr_pref_int_set(TR_KEY_main_window_is_maximized, isMaximized);
335 
336     if (!isMaximized)
337     {
338         int x;
339         int y;
340         int w;
341         int h;
342         gtk_window_get_position(GTK_WINDOW(gtk_window), &x, &y);
343         gtk_window_get_size(GTK_WINDOW(gtk_window), &w, &h);
344         gtr_pref_int_set(TR_KEY_main_window_x, x);
345         gtr_pref_int_set(TR_KEY_main_window_y, y);
346         gtr_pref_int_set(TR_KEY_main_window_width, w);
347         gtr_pref_int_set(TR_KEY_main_window_height, h);
348     }
349 }
350 
351 /***
352 **** listen to changes that come from RPC
353 ***/
354 
355 struct on_rpc_changed_struct
356 {
357     TrCore* core;
358     tr_rpc_callback_type type;
359     int torrent_id;
360 };
361 
on_rpc_changed_idle(gpointer gdata)362 static gboolean on_rpc_changed_idle(gpointer gdata)
363 {
364     tr_torrent* tor;
365     struct on_rpc_changed_struct* data = gdata;
366 
367     switch (data->type)
368     {
369     case TR_RPC_SESSION_CLOSE:
370         gtr_action_activate("quit");
371         break;
372 
373     case TR_RPC_TORRENT_ADDED:
374         if ((tor = gtr_core_find_torrent(data->core, data->torrent_id)) != NULL)
375         {
376             gtr_core_add_torrent(data->core, tor, true);
377         }
378 
379         break;
380 
381     case TR_RPC_TORRENT_REMOVING:
382         gtr_core_remove_torrent(data->core, data->torrent_id, false);
383         break;
384 
385     case TR_RPC_TORRENT_TRASHING:
386         gtr_core_remove_torrent(data->core, data->torrent_id, true);
387         break;
388 
389     case TR_RPC_SESSION_CHANGED:
390         {
391             tr_variant tmp;
392             tr_variant* newval;
393             tr_variant* oldvals = gtr_pref_get_all();
394             tr_quark key;
395             GSList* changed_keys = NULL;
396             tr_session* session = gtr_core_session(data->core);
397             tr_variantInitDict(&tmp, 100);
398             tr_sessionGetSettings(session, &tmp);
399 
400             for (int i = 0; tr_variantDictChild(&tmp, i, &key, &newval); ++i)
401             {
402                 bool changed;
403                 tr_variant* oldval = tr_variantDictFind(oldvals, key);
404 
405                 if (oldval == NULL)
406                 {
407                     changed = true;
408                 }
409                 else
410                 {
411                     char* a = tr_variantToStr(oldval, TR_VARIANT_FMT_BENC, NULL);
412                     char* b = tr_variantToStr(newval, TR_VARIANT_FMT_BENC, NULL);
413                     changed = g_strcmp0(a, b) != 0;
414                     tr_free(b);
415                     tr_free(a);
416                 }
417 
418                 if (changed)
419                 {
420                     changed_keys = g_slist_append(changed_keys, GINT_TO_POINTER(key));
421                 }
422             }
423 
424             tr_sessionGetSettings(session, oldvals);
425 
426             for (GSList* l = changed_keys; l != NULL; l = l->next)
427             {
428                 gtr_core_pref_changed(data->core, GPOINTER_TO_INT(l->data));
429             }
430 
431             g_slist_free(changed_keys);
432             tr_variantFree(&tmp);
433             break;
434         }
435 
436     case TR_RPC_TORRENT_CHANGED:
437     case TR_RPC_TORRENT_MOVED:
438     case TR_RPC_TORRENT_STARTED:
439     case TR_RPC_TORRENT_STOPPED:
440     case TR_RPC_SESSION_QUEUE_POSITIONS_CHANGED:
441         /* nothing interesting to do here */
442         break;
443     }
444 
445     g_free(data);
446     return G_SOURCE_REMOVE;
447 }
448 
on_rpc_changed(tr_session * session G_GNUC_UNUSED,tr_rpc_callback_type type,struct tr_torrent * tor,void * gdata)449 static tr_rpc_callback_status on_rpc_changed(tr_session* session G_GNUC_UNUSED, tr_rpc_callback_type type,
450     struct tr_torrent* tor, void* gdata)
451 {
452     struct cbdata* cbdata = gdata;
453     struct on_rpc_changed_struct* data;
454 
455     data = g_new(struct on_rpc_changed_struct, 1);
456     data->core = cbdata->core;
457     data->type = type;
458     data->torrent_id = tr_torrentId(tor);
459     gdk_threads_add_idle(on_rpc_changed_idle, data);
460 
461     return TR_RPC_NOREMOVE;
462 }
463 
464 /***
465 ****  signal handling
466 ***/
467 
468 static sig_atomic_t global_sigcount = 0;
469 static struct cbdata* sighandler_cbdata = NULL;
470 
signal_handler(int sig)471 static void signal_handler(int sig)
472 {
473     if (++global_sigcount > 1)
474     {
475         signal(sig, SIG_DFL);
476         raise(sig);
477     }
478     else if (sig == SIGINT || sig == SIGTERM)
479     {
480         g_message(_("Got signal %d; trying to shut down cleanly. Do it again if it gets stuck."), sig);
481         gtr_actions_handler("quit", sighandler_cbdata);
482     }
483 }
484 
485 /****
486 *****
487 *****
488 ****/
489 
490 static void app_setup(GtkWindow* wind, struct cbdata* cbdata);
491 
on_startup(GApplication * application,gpointer user_data)492 static void on_startup(GApplication* application, gpointer user_data)
493 {
494     GError* error;
495     char const* str;
496     GtkWindow* win;
497     GtkUIManager* ui_manager;
498     tr_session* session;
499     struct cbdata* cbdata = user_data;
500 
501     signal(SIGINT, signal_handler);
502     signal(SIGTERM, signal_handler);
503 
504     sighandler_cbdata = cbdata;
505 
506     /* ensure the directories are created */
507     if ((str = gtr_pref_string_get(TR_KEY_download_dir)) != NULL)
508     {
509         g_mkdir_with_parents(str, 0777);
510     }
511 
512     if ((str = gtr_pref_string_get(TR_KEY_incomplete_dir)) != NULL)
513     {
514         g_mkdir_with_parents(str, 0777);
515     }
516 
517     /* initialize the libtransmission session */
518     session = tr_sessionInit(cbdata->config_dir, TRUE, gtr_pref_get_all());
519 
520     gtr_pref_flag_set(TR_KEY_alt_speed_enabled, tr_sessionUsesAltSpeed(session));
521     gtr_pref_int_set(TR_KEY_peer_port, tr_sessionGetPeerPort(session));
522     cbdata->core = gtr_core_new(session);
523 
524     /* init the ui manager */
525     error = NULL;
526     ui_manager = gtk_ui_manager_new();
527     gtr_actions_init(ui_manager, cbdata);
528     gtk_ui_manager_add_ui_from_resource(ui_manager, TR_RESOURCE_PATH "transmission-ui.xml", &error);
529     g_assert_no_error(error);
530     gtk_ui_manager_ensure_update(ui_manager);
531 
532     /* create main window now to be a parent to any error dialogs */
533     win = GTK_WINDOW(gtr_window_new(GTK_APPLICATION(application), ui_manager, cbdata->core));
534     g_signal_connect(win, "size-allocate", G_CALLBACK(on_main_window_size_allocated), cbdata);
535     g_application_hold(application);
536     g_object_weak_ref(G_OBJECT(win), (GWeakNotify)(GCallback)g_application_release, application);
537     app_setup(win, cbdata);
538     tr_sessionSetRPCCallback(session, on_rpc_changed, cbdata);
539 
540     /* check & see if it's time to update the blocklist */
541     if (gtr_pref_flag_get(TR_KEY_blocklist_enabled))
542     {
543         if (gtr_pref_flag_get(TR_KEY_blocklist_updates_enabled))
544         {
545             int64_t const last_time = gtr_pref_int_get(TR_KEY_blocklist_date);
546             int const SECONDS_IN_A_WEEK = 7 * 24 * 60 * 60;
547             time_t const now = time(NULL);
548 
549             if (last_time + SECONDS_IN_A_WEEK < now)
550             {
551                 gtr_core_blocklist_update(cbdata->core);
552             }
553         }
554     }
555 
556     /* if there's no magnet link handler registered, register us */
557     ensure_magnet_handler_exists();
558 }
559 
on_activate(GApplication * app UNUSED,struct cbdata * cbdata)560 static void on_activate(GApplication* app UNUSED, struct cbdata* cbdata)
561 {
562     cbdata->activation_count++;
563 
564     /* GApplication emits an 'activate' signal when bootstrapping the primary.
565      * Ordinarily we handle that by presenting the main window, but if the user
566      * user started Transmission minimized, ignore that initial signal... */
567     if (cbdata->is_iconified && cbdata->activation_count == 1)
568     {
569         return;
570     }
571 
572     gtr_action_activate("present-main-window");
573 }
574 
open_files(GSList * files,gpointer gdata)575 static void open_files(GSList* files, gpointer gdata)
576 {
577     struct cbdata* cbdata = gdata;
578     gboolean const do_start = gtr_pref_flag_get(TR_KEY_start_added_torrents) && !cbdata->start_paused;
579     gboolean const do_prompt = gtr_pref_flag_get(TR_KEY_show_options_window);
580     gboolean const do_notify = TRUE;
581 
582     gtr_core_add_files(cbdata->core, files, do_start, do_prompt, do_notify);
583 }
584 
on_open(GApplication * application UNUSED,GFile ** f,gint file_count,gchar * hint UNUSED,gpointer gdata)585 static void on_open(GApplication* application UNUSED, GFile** f, gint file_count, gchar* hint UNUSED, gpointer gdata)
586 {
587     GSList* files = NULL;
588 
589     for (gint i = 0; i < file_count; i++)
590     {
591         files = g_slist_prepend(files, f[i]);
592     }
593 
594     open_files(files, gdata);
595 
596     g_slist_free(files);
597 }
598 
599 /***
600 ****
601 ***/
602 
main(int argc,char ** argv)603 int main(int argc, char** argv)
604 {
605     int ret;
606     struct stat sb;
607     char* application_id;
608     GtkApplication* app;
609     GOptionContext* option_context;
610     bool show_version = false;
611     GError* error = NULL;
612     struct cbdata cbdata;
613 
614     GOptionEntry option_entries[] =
615     {
616         { "config-dir", 'g', 0, G_OPTION_ARG_FILENAME, &cbdata.config_dir, _("Where to look for configuration files"), NULL },
617         { "paused", 'p', 0, G_OPTION_ARG_NONE, &cbdata.start_paused, _("Start with all torrents paused"), NULL },
618         { "minimized", 'm', 0, G_OPTION_ARG_NONE, &cbdata.is_iconified, _("Start minimized in notification area"), NULL },
619         { "version", 'v', 0, G_OPTION_ARG_NONE, &show_version, _("Show version number and exit"), NULL },
620         { NULL, 0, 0, 0, NULL, NULL, NULL }
621     };
622 
623     /* default settings */
624     memset(&cbdata, 0, sizeof(struct cbdata));
625     cbdata.config_dir = (char*)tr_getDefaultConfigDir(MY_CONFIG_NAME);
626 
627     /* init i18n */
628     setlocale(LC_ALL, "");
629     bindtextdomain(MY_READABLE_NAME, TRANSMISSIONLOCALEDIR);
630     bind_textdomain_codeset(MY_READABLE_NAME, "UTF-8");
631     textdomain(MY_READABLE_NAME);
632 
633     /* init glib/gtk */
634 #if !GLIB_CHECK_VERSION(2, 35, 4)
635     g_type_init();
636 #endif
637     g_set_application_name(_("Transmission"));
638 
639     /* parse the command line */
640     option_context = g_option_context_new(_("[torrent files or urls]"));
641     g_option_context_add_main_entries(option_context, option_entries, GETTEXT_PACKAGE);
642     g_option_context_add_group(option_context, gtk_get_option_group(FALSE));
643     g_option_context_set_translation_domain(option_context, GETTEXT_PACKAGE);
644 
645     if (!g_option_context_parse(option_context, &argc, &argv, &error))
646     {
647         g_print(_("%s\nRun '%s --help' to see a full list of available command line options.\n"), error->message, argv[0]);
648         g_error_free(error);
649         g_option_context_free(option_context);
650         return 1;
651     }
652 
653     g_option_context_free(option_context);
654 
655     /* handle the trivial "version" option */
656     if (show_version)
657     {
658         fprintf(stderr, "%s %s\n", MY_READABLE_NAME, LONG_VERSION_STRING);
659         return 0;
660     }
661 
662     gtk_window_set_default_icon_name(MY_CONFIG_NAME);
663 
664     /* init the unit formatters */
665     tr_formatter_mem_init(mem_K, _(mem_K_str), _(mem_M_str), _(mem_G_str), _(mem_T_str));
666     tr_formatter_size_init(disk_K, _(disk_K_str), _(disk_M_str), _(disk_G_str), _(disk_T_str));
667     tr_formatter_speed_init(speed_K, _(speed_K_str), _(speed_M_str), _(speed_G_str), _(speed_T_str));
668 
669     /* set up the config dir */
670     gtr_pref_init(cbdata.config_dir);
671     g_mkdir_with_parents(cbdata.config_dir, 0755);
672 
673     /* init notifications */
674     gtr_notify_init();
675 
676     /* init the application for the specified config dir */
677     stat(cbdata.config_dir, &sb);
678     application_id = g_strdup_printf("com.transmissionbt.transmission_%lu_%lu", (unsigned long)sb.st_dev,
679         (unsigned long)sb.st_ino);
680     app = gtk_application_new(application_id, G_APPLICATION_HANDLES_OPEN);
681     g_signal_connect(app, "open", G_CALLBACK(on_open), &cbdata);
682     g_signal_connect(app, "startup", G_CALLBACK(on_startup), &cbdata);
683     g_signal_connect(app, "activate", G_CALLBACK(on_activate), &cbdata);
684     ret = g_application_run(G_APPLICATION(app), argc, argv);
685     g_object_unref(app);
686     g_free(application_id);
687     return ret;
688 }
689 
on_core_busy(TrCore * core UNUSED,gboolean busy,struct cbdata * c)690 static void on_core_busy(TrCore* core UNUSED, gboolean busy, struct cbdata* c)
691 {
692     gtr_window_set_busy(c->wind, busy);
693 }
694 
695 static void on_core_error(TrCore*, guint, char const*, struct cbdata*);
696 static void on_add_torrent(TrCore*, tr_ctor*, gpointer);
697 static void on_prefs_changed(TrCore* core, tr_quark const key, gpointer);
698 static void main_window_setup(struct cbdata* cbdata, GtkWindow* wind);
699 static gboolean update_model_loop(gpointer gdata);
700 static gboolean update_model_once(gpointer gdata);
701 
app_setup(GtkWindow * wind,struct cbdata * cbdata)702 static void app_setup(GtkWindow* wind, struct cbdata* cbdata)
703 {
704     if (cbdata->is_iconified)
705     {
706         gtr_pref_flag_set(TR_KEY_show_notification_area_icon, TRUE);
707     }
708 
709     gtr_actions_set_core(cbdata->core);
710 
711     /* set up core handlers */
712     g_signal_connect(cbdata->core, "busy", G_CALLBACK(on_core_busy), cbdata);
713     g_signal_connect(cbdata->core, "add-error", G_CALLBACK(on_core_error), cbdata);
714     g_signal_connect(cbdata->core, "add-prompt", G_CALLBACK(on_add_torrent), cbdata);
715     g_signal_connect(cbdata->core, "prefs-changed", G_CALLBACK(on_prefs_changed), cbdata);
716 
717     /* add torrents from command-line and saved state */
718     gtr_core_load(cbdata->core, cbdata->start_paused);
719     gtr_core_torrents_added(cbdata->core);
720 
721     /* set up main window */
722     main_window_setup(cbdata, wind);
723 
724     /* set up the icon */
725     on_prefs_changed(cbdata->core, TR_KEY_show_notification_area_icon, cbdata);
726 
727     /* start model update timer */
728     cbdata->timer = gdk_threads_add_timeout_seconds(MAIN_WINDOW_REFRESH_INTERVAL_SECONDS, update_model_loop, cbdata);
729     update_model_once(cbdata);
730 
731     /* either show the window or iconify it */
732     if (!cbdata->is_iconified)
733     {
734         gtk_widget_show(GTK_WIDGET(wind));
735     }
736     else
737     {
738         gtk_window_set_skip_taskbar_hint(cbdata->wind, cbdata->icon != NULL);
739         cbdata->is_iconified = FALSE; // ensure that the next toggle iconifies
740         gtr_action_set_toggled("toggle-main-window", FALSE);
741     }
742 
743     if (!gtr_pref_flag_get(TR_KEY_user_has_given_informed_consent))
744     {
745         GtkWidget* w = gtk_message_dialog_new(GTK_WINDOW(wind), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_OTHER,
746             GTK_BUTTONS_NONE, "%s", _("Transmission is a file sharing program. When you run a torrent, its data will be "
747             "made available to others by means of upload. Any content you share is your sole responsibility."));
748         gtk_dialog_add_button(GTK_DIALOG(w), GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT);
749         gtk_dialog_add_button(GTK_DIALOG(w), _("I _Agree"), GTK_RESPONSE_ACCEPT);
750         gtk_dialog_set_default_response(GTK_DIALOG(w), GTK_RESPONSE_ACCEPT);
751 
752         switch (gtk_dialog_run(GTK_DIALOG(w)))
753         {
754         case GTK_RESPONSE_ACCEPT:
755             /* only show it once */
756             gtr_pref_flag_set(TR_KEY_user_has_given_informed_consent, TRUE);
757             gtk_widget_destroy(w);
758             break;
759 
760         default:
761             exit(0);
762         }
763     }
764 }
765 
presentMainWindow(struct cbdata * cbdata)766 static void presentMainWindow(struct cbdata* cbdata)
767 {
768     GtkWindow* window = cbdata->wind;
769 
770     if (cbdata->is_iconified)
771     {
772         cbdata->is_iconified = false;
773 
774         gtk_window_set_skip_taskbar_hint(window, FALSE);
775     }
776 
777     if (!gtk_widget_get_visible(GTK_WIDGET(window)))
778     {
779         gtk_window_resize(window, gtr_pref_int_get(TR_KEY_main_window_width), gtr_pref_int_get(TR_KEY_main_window_height));
780         gtk_window_move(window, gtr_pref_int_get(TR_KEY_main_window_x), gtr_pref_int_get(TR_KEY_main_window_y));
781         gtr_widget_set_visible(GTK_WIDGET(window), TRUE);
782     }
783 
784     gtr_window_present(window);
785     gdk_window_raise(gtk_widget_get_window(GTK_WIDGET(window)));
786 }
787 
hideMainWindow(struct cbdata * cbdata)788 static void hideMainWindow(struct cbdata* cbdata)
789 {
790     GtkWindow* window = cbdata->wind;
791     gtk_window_set_skip_taskbar_hint(window, TRUE);
792     gtr_widget_set_visible(GTK_WIDGET(window), FALSE);
793     cbdata->is_iconified = true;
794 }
795 
toggleMainWindow(struct cbdata * cbdata)796 static void toggleMainWindow(struct cbdata* cbdata)
797 {
798     if (cbdata->is_iconified)
799     {
800         presentMainWindow(cbdata);
801     }
802     else
803     {
804         hideMainWindow(cbdata);
805     }
806 }
807 
808 static void on_app_exit(gpointer vdata);
809 
winclose(GtkWidget * w UNUSED,GdkEvent * event UNUSED,gpointer gdata)810 static gboolean winclose(GtkWidget* w UNUSED, GdkEvent* event UNUSED, gpointer gdata)
811 {
812     struct cbdata* cbdata = gdata;
813 
814     if (cbdata->icon != NULL)
815     {
816         gtr_action_activate("toggle-main-window");
817     }
818     else
819     {
820         on_app_exit(cbdata);
821     }
822 
823     return TRUE; /* don't propagate event further */
824 }
825 
rowChangedCB(GtkTreeModel * model UNUSED,GtkTreePath * path,GtkTreeIter * iter UNUSED,gpointer gdata)826 static void rowChangedCB(GtkTreeModel* model UNUSED, GtkTreePath* path, GtkTreeIter* iter UNUSED, gpointer gdata)
827 {
828     struct cbdata* data = gdata;
829 
830     if (gtk_tree_selection_path_is_selected(data->sel, path))
831     {
832         refresh_actions_soon(data);
833     }
834 }
835 
on_drag_data_received(GtkWidget * widget UNUSED,GdkDragContext * drag_context,gint x UNUSED,gint y UNUSED,GtkSelectionData * selection_data,guint info UNUSED,guint time_,gpointer gdata)836 static void on_drag_data_received(GtkWidget* widget UNUSED, GdkDragContext* drag_context, gint x UNUSED, gint y UNUSED,
837     GtkSelectionData* selection_data, guint info UNUSED, guint time_, gpointer gdata)
838 {
839     char** uris = gtk_selection_data_get_uris(selection_data);
840     guint const file_count = g_strv_length(uris);
841     GSList* files = NULL;
842 
843     for (guint i = 0; i < file_count; ++i)
844     {
845         files = g_slist_prepend(files, g_file_new_for_uri(uris[i]));
846     }
847 
848     open_files(files, gdata);
849 
850     /* cleanup */
851     g_slist_foreach(files, (GFunc)(GCallback)g_object_unref, NULL);
852     g_slist_free(files);
853     g_strfreev(uris);
854 
855     gtk_drag_finish(drag_context, true, FALSE, time_);
856 }
857 
main_window_setup(struct cbdata * cbdata,GtkWindow * wind)858 static void main_window_setup(struct cbdata* cbdata, GtkWindow* wind)
859 {
860     GtkWidget* w;
861     GtkTreeModel* model;
862     GtkTreeSelection* sel;
863 
864     g_assert(NULL == cbdata->wind);
865     cbdata->wind = wind;
866     cbdata->sel = sel = GTK_TREE_SELECTION(gtr_window_get_selection(cbdata->wind));
867 
868     g_signal_connect(sel, "changed", G_CALLBACK(on_selection_changed), cbdata);
869     on_selection_changed(sel, cbdata);
870     model = gtr_core_model(cbdata->core);
871     g_signal_connect(model, "row-changed", G_CALLBACK(rowChangedCB), cbdata);
872     g_signal_connect(wind, "delete-event", G_CALLBACK(winclose), cbdata);
873     refresh_actions(cbdata);
874 
875     /* register to handle URIs that get dragged onto our main window */
876     w = GTK_WIDGET(wind);
877     gtk_drag_dest_set(w, GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY);
878     gtk_drag_dest_add_uri_targets(w);
879     g_signal_connect(w, "drag-data-received", G_CALLBACK(on_drag_data_received), cbdata);
880 }
881 
on_session_closed(gpointer gdata)882 static gboolean on_session_closed(gpointer gdata)
883 {
884     GSList* tmp;
885     struct cbdata* cbdata = gdata;
886 
887     tmp = g_slist_copy(cbdata->details);
888     g_slist_foreach(tmp, (GFunc)(GCallback)gtk_widget_destroy, NULL);
889     g_slist_free(tmp);
890 
891     if (cbdata->prefs != NULL)
892     {
893         gtk_widget_destroy(GTK_WIDGET(cbdata->prefs));
894     }
895 
896     if (cbdata->wind != NULL)
897     {
898         gtk_widget_destroy(GTK_WIDGET(cbdata->wind));
899     }
900 
901     g_object_unref(cbdata->core);
902 
903     if (cbdata->icon != NULL)
904     {
905         g_object_unref(cbdata->icon);
906     }
907 
908     g_slist_foreach(cbdata->error_list, (GFunc)(GCallback)g_free, NULL);
909     g_slist_free(cbdata->error_list);
910     g_slist_foreach(cbdata->duplicates_list, (GFunc)(GCallback)g_free, NULL);
911     g_slist_free(cbdata->duplicates_list);
912 
913     return G_SOURCE_REMOVE;
914 }
915 
916 struct session_close_struct
917 {
918     tr_session* session;
919     struct cbdata* cbdata;
920 };
921 
922 /* since tr_sessionClose () is a blocking function,
923  * delegate its call to another thread here... when it's done,
924  * punt the GUI teardown back to the GTK+ thread */
session_close_threadfunc(gpointer gdata)925 static gpointer session_close_threadfunc(gpointer gdata)
926 {
927     struct session_close_struct* data = gdata;
928     tr_sessionClose(data->session);
929     gdk_threads_add_idle(on_session_closed, data->cbdata);
930     g_free(data);
931     return NULL;
932 }
933 
exit_now_cb(GtkWidget * w UNUSED,gpointer data UNUSED)934 static void exit_now_cb(GtkWidget* w UNUSED, gpointer data UNUSED)
935 {
936     exit(0);
937 }
938 
on_app_exit(gpointer vdata)939 static void on_app_exit(gpointer vdata)
940 {
941     GtkWidget* p;
942     GtkWidget* w;
943     GtkWidget* c;
944     struct cbdata* cbdata = vdata;
945     struct session_close_struct* session_close_data;
946 
947     if (cbdata->is_closing)
948     {
949         return;
950     }
951 
952     cbdata->is_closing = true;
953 
954     /* stop the update timer */
955     if (cbdata->timer != 0)
956     {
957         g_source_remove(cbdata->timer);
958         cbdata->timer = 0;
959     }
960 
961     /* stop the refresh-actions timer */
962     if (cbdata->refresh_actions_tag != 0)
963     {
964         g_source_remove(cbdata->refresh_actions_tag);
965         cbdata->refresh_actions_tag = 0;
966     }
967 
968     c = GTK_WIDGET(cbdata->wind);
969     gtk_container_remove(GTK_CONTAINER(c), gtk_bin_get_child(GTK_BIN(c)));
970 
971     p =
972         g_object_new(GTK_TYPE_GRID, "column-spacing", GUI_PAD_BIG, "halign", GTK_ALIGN_CENTER, "valign", GTK_ALIGN_CENTER,
973         NULL);
974     gtk_container_add(GTK_CONTAINER(c), p);
975 
976     w = gtk_image_new_from_icon_name(GTK_STOCK_NETWORK, GTK_ICON_SIZE_DIALOG);
977     gtk_grid_attach(GTK_GRID(p), w, 0, 0, 1, 2);
978 
979     w = gtk_label_new(NULL);
980     gtk_label_set_markup(GTK_LABEL(w), _("<b>Closing Connections</b>"));
981     g_object_set(w, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_CENTER, NULL);
982     gtk_grid_attach(GTK_GRID(p), w, 1, 0, 1, 1);
983 
984     w = gtk_label_new(_("Sending upload/download totals to tracker…"));
985     g_object_set(w, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_CENTER, NULL);
986     gtk_grid_attach(GTK_GRID(p), w, 1, 1, 1, 1);
987 
988     w = gtk_button_new_with_mnemonic(_("_Quit Now"));
989     g_object_set(w, "margin-top", GUI_PAD, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_END, NULL);
990     g_signal_connect(w, "clicked", G_CALLBACK(exit_now_cb), NULL);
991     gtk_grid_attach(GTK_GRID(p), w, 1, 2, 1, 1);
992 
993     gtk_widget_show_all(p);
994     gtk_widget_grab_focus(w);
995 
996     /* clear the UI */
997     gtr_core_clear(cbdata->core);
998 
999     /* ensure the window is in its previous position & size.
1000      * this seems to be necessary because changing the main window's
1001      * child seems to unset the size */
1002     gtk_window_resize(cbdata->wind, gtr_pref_int_get(TR_KEY_main_window_width), gtr_pref_int_get(TR_KEY_main_window_height));
1003     gtk_window_move(cbdata->wind, gtr_pref_int_get(TR_KEY_main_window_x), gtr_pref_int_get(TR_KEY_main_window_y));
1004 
1005     /* shut down libT */
1006     session_close_data = g_new(struct session_close_struct, 1);
1007     session_close_data->cbdata = cbdata;
1008     session_close_data->session = gtr_core_close(cbdata->core);
1009     g_thread_new("shutdown-thread", session_close_threadfunc, session_close_data);
1010 }
1011 
show_torrent_errors(GtkWindow * window,char const * primary,GSList ** files)1012 static void show_torrent_errors(GtkWindow* window, char const* primary, GSList** files)
1013 {
1014     GtkWidget* w;
1015     GString* s = g_string_new(NULL);
1016     char const* leader = g_slist_length(*files) > 1 ? gtr_get_unicode_string(GTR_UNICODE_BULLET) : "";
1017 
1018     for (GSList* l = *files; l != NULL; l = l->next)
1019     {
1020         g_string_append_printf(s, "%s %s\n", leader, (char const*)l->data);
1021     }
1022 
1023     w = gtk_message_dialog_new(window, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", primary);
1024     gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(w), "%s", s->str);
1025     g_signal_connect_swapped(w, "response", G_CALLBACK(gtk_widget_destroy), w);
1026     gtk_widget_show(w);
1027     g_string_free(s, TRUE);
1028 
1029     g_slist_foreach(*files, (GFunc)(GCallback)g_free, NULL);
1030     g_slist_free(*files);
1031     *files = NULL;
1032 }
1033 
flush_torrent_errors(struct cbdata * cbdata)1034 static void flush_torrent_errors(struct cbdata* cbdata)
1035 {
1036     if (cbdata->error_list != NULL)
1037     {
1038         show_torrent_errors(cbdata->wind, ngettext("Couldn't add corrupt torrent", "Couldn't add corrupt torrents",
1039             g_slist_length(cbdata->error_list)), &cbdata->error_list);
1040     }
1041 
1042     if (cbdata->duplicates_list != NULL)
1043     {
1044         show_torrent_errors(cbdata->wind, ngettext("Couldn't add duplicate torrent", "Couldn't add duplicate torrents",
1045             g_slist_length(cbdata->duplicates_list)), &cbdata->duplicates_list);
1046     }
1047 }
1048 
on_core_error(TrCore * core UNUSED,guint code,char const * msg,struct cbdata * c)1049 static void on_core_error(TrCore* core UNUSED, guint code, char const* msg, struct cbdata* c)
1050 {
1051     switch (code)
1052     {
1053     case TR_PARSE_ERR:
1054         c->error_list = g_slist_append(c->error_list, g_path_get_basename(msg));
1055         break;
1056 
1057     case TR_PARSE_DUPLICATE:
1058         c->duplicates_list = g_slist_append(c->duplicates_list, g_strdup(msg));
1059         break;
1060 
1061     case TR_CORE_ERR_NO_MORE_TORRENTS:
1062         flush_torrent_errors(c);
1063         break;
1064 
1065     default:
1066         g_assert_not_reached();
1067         break;
1068     }
1069 }
1070 
on_main_window_focus_in(GtkWidget * widget UNUSED,GdkEventFocus * event UNUSED,gpointer gdata)1071 static gboolean on_main_window_focus_in(GtkWidget* widget UNUSED, GdkEventFocus* event UNUSED, gpointer gdata)
1072 {
1073     struct cbdata* cbdata = gdata;
1074 
1075     if (cbdata->wind != NULL)
1076     {
1077         gtk_window_set_urgency_hint(cbdata->wind, FALSE);
1078     }
1079 
1080     return FALSE;
1081 }
1082 
on_add_torrent(TrCore * core,tr_ctor * ctor,gpointer gdata)1083 static void on_add_torrent(TrCore* core, tr_ctor* ctor, gpointer gdata)
1084 {
1085     struct cbdata* cbdata = gdata;
1086     GtkWidget* w = gtr_torrent_options_dialog_new(cbdata->wind, core, ctor);
1087 
1088     g_signal_connect(w, "focus-in-event", G_CALLBACK(on_main_window_focus_in), cbdata);
1089 
1090     if (cbdata->wind != NULL)
1091     {
1092         gtk_window_set_urgency_hint(cbdata->wind, TRUE);
1093     }
1094 
1095     gtk_widget_show(w);
1096 }
1097 
on_prefs_changed(TrCore * core UNUSED,tr_quark const key,gpointer data)1098 static void on_prefs_changed(TrCore* core UNUSED, tr_quark const key, gpointer data)
1099 {
1100     struct cbdata* cbdata = data;
1101     tr_session* tr = gtr_core_session(cbdata->core);
1102 
1103     switch (key)
1104     {
1105     case TR_KEY_encryption:
1106         tr_sessionSetEncryption(tr, gtr_pref_int_get(key));
1107         break;
1108 
1109     case TR_KEY_download_dir:
1110         tr_sessionSetDownloadDir(tr, gtr_pref_string_get(key));
1111         break;
1112 
1113     case TR_KEY_message_level:
1114         tr_logSetLevel(gtr_pref_int_get(key));
1115         break;
1116 
1117     case TR_KEY_peer_port:
1118         tr_sessionSetPeerPort(tr, gtr_pref_int_get(key));
1119         break;
1120 
1121     case TR_KEY_blocklist_enabled:
1122         tr_blocklistSetEnabled(tr, gtr_pref_flag_get(key));
1123         break;
1124 
1125     case TR_KEY_blocklist_url:
1126         tr_blocklistSetURL(tr, gtr_pref_string_get(key));
1127         break;
1128 
1129     case TR_KEY_show_notification_area_icon:
1130         {
1131             bool const show = gtr_pref_flag_get(key);
1132 
1133             if (show && cbdata->icon == NULL)
1134             {
1135                 cbdata->icon = gtr_icon_new(cbdata->core);
1136             }
1137             else if (!show && cbdata->icon != NULL)
1138             {
1139                 g_clear_object(&cbdata->icon);
1140             }
1141 
1142             break;
1143         }
1144 
1145     case TR_KEY_speed_limit_down_enabled:
1146         tr_sessionLimitSpeed(tr, TR_DOWN, gtr_pref_flag_get(key));
1147         break;
1148 
1149     case TR_KEY_speed_limit_down:
1150         tr_sessionSetSpeedLimit_KBps(tr, TR_DOWN, gtr_pref_int_get(key));
1151         break;
1152 
1153     case TR_KEY_speed_limit_up_enabled:
1154         tr_sessionLimitSpeed(tr, TR_UP, gtr_pref_flag_get(key));
1155         break;
1156 
1157     case TR_KEY_speed_limit_up:
1158         tr_sessionSetSpeedLimit_KBps(tr, TR_UP, gtr_pref_int_get(key));
1159         break;
1160 
1161     case TR_KEY_ratio_limit_enabled:
1162         tr_sessionSetRatioLimited(tr, gtr_pref_flag_get(key));
1163         break;
1164 
1165     case TR_KEY_ratio_limit:
1166         tr_sessionSetRatioLimit(tr, gtr_pref_double_get(key));
1167         break;
1168 
1169     case TR_KEY_idle_seeding_limit:
1170         tr_sessionSetIdleLimit(tr, gtr_pref_int_get(key));
1171         break;
1172 
1173     case TR_KEY_idle_seeding_limit_enabled:
1174         tr_sessionSetIdleLimited(tr, gtr_pref_flag_get(key));
1175         break;
1176 
1177     case TR_KEY_port_forwarding_enabled:
1178         tr_sessionSetPortForwardingEnabled(tr, gtr_pref_flag_get(key));
1179         break;
1180 
1181     case TR_KEY_pex_enabled:
1182         tr_sessionSetPexEnabled(tr, gtr_pref_flag_get(key));
1183         break;
1184 
1185     case TR_KEY_rename_partial_files:
1186         tr_sessionSetIncompleteFileNamingEnabled(tr, gtr_pref_flag_get(key));
1187         break;
1188 
1189     case TR_KEY_download_queue_size:
1190         tr_sessionSetQueueSize(tr, TR_DOWN, gtr_pref_int_get(key));
1191         break;
1192 
1193     case TR_KEY_queue_stalled_minutes:
1194         tr_sessionSetQueueStalledMinutes(tr, gtr_pref_int_get(key));
1195         break;
1196 
1197     case TR_KEY_dht_enabled:
1198         tr_sessionSetDHTEnabled(tr, gtr_pref_flag_get(key));
1199         break;
1200 
1201     case TR_KEY_utp_enabled:
1202         tr_sessionSetUTPEnabled(tr, gtr_pref_flag_get(key));
1203         break;
1204 
1205     case TR_KEY_lpd_enabled:
1206         tr_sessionSetLPDEnabled(tr, gtr_pref_flag_get(key));
1207         break;
1208 
1209     case TR_KEY_rpc_port:
1210         tr_sessionSetRPCPort(tr, gtr_pref_int_get(key));
1211         break;
1212 
1213     case TR_KEY_rpc_enabled:
1214         tr_sessionSetRPCEnabled(tr, gtr_pref_flag_get(key));
1215         break;
1216 
1217     case TR_KEY_rpc_whitelist:
1218         tr_sessionSetRPCWhitelist(tr, gtr_pref_string_get(key));
1219         break;
1220 
1221     case TR_KEY_rpc_whitelist_enabled:
1222         tr_sessionSetRPCWhitelistEnabled(tr, gtr_pref_flag_get(key));
1223         break;
1224 
1225     case TR_KEY_rpc_username:
1226         tr_sessionSetRPCUsername(tr, gtr_pref_string_get(key));
1227         break;
1228 
1229     case TR_KEY_rpc_password:
1230         tr_sessionSetRPCPassword(tr, gtr_pref_string_get(key));
1231         break;
1232 
1233     case TR_KEY_rpc_authentication_required:
1234         tr_sessionSetRPCPasswordEnabled(tr, gtr_pref_flag_get(key));
1235         break;
1236 
1237     case TR_KEY_alt_speed_up:
1238         tr_sessionSetAltSpeed_KBps(tr, TR_UP, gtr_pref_int_get(key));
1239         break;
1240 
1241     case TR_KEY_alt_speed_down:
1242         tr_sessionSetAltSpeed_KBps(tr, TR_DOWN, gtr_pref_int_get(key));
1243         break;
1244 
1245     case TR_KEY_alt_speed_enabled:
1246         {
1247             bool const b = gtr_pref_flag_get(key);
1248             tr_sessionUseAltSpeed(tr, b);
1249             gtr_action_set_toggled(tr_quark_get_string(key, NULL), b);
1250             break;
1251         }
1252 
1253     case TR_KEY_alt_speed_time_begin:
1254         tr_sessionSetAltSpeedBegin(tr, gtr_pref_int_get(key));
1255         break;
1256 
1257     case TR_KEY_alt_speed_time_end:
1258         tr_sessionSetAltSpeedEnd(tr, gtr_pref_int_get(key));
1259         break;
1260 
1261     case TR_KEY_alt_speed_time_enabled:
1262         tr_sessionUseAltSpeedTime(tr, gtr_pref_flag_get(key));
1263         break;
1264 
1265     case TR_KEY_alt_speed_time_day:
1266         tr_sessionSetAltSpeedDay(tr, gtr_pref_int_get(key));
1267         break;
1268 
1269     case TR_KEY_peer_port_random_on_start:
1270         tr_sessionSetPeerPortRandomOnStart(tr, gtr_pref_flag_get(key));
1271         break;
1272 
1273     case TR_KEY_incomplete_dir:
1274         tr_sessionSetIncompleteDir(tr, gtr_pref_string_get(key));
1275         break;
1276 
1277     case TR_KEY_incomplete_dir_enabled:
1278         tr_sessionSetIncompleteDirEnabled(tr, gtr_pref_flag_get(key));
1279         break;
1280 
1281     case TR_KEY_script_torrent_done_enabled:
1282         tr_sessionSetTorrentDoneScriptEnabled(tr, gtr_pref_flag_get(key));
1283         break;
1284 
1285     case TR_KEY_script_torrent_done_filename:
1286         tr_sessionSetTorrentDoneScript(tr, gtr_pref_string_get(key));
1287         break;
1288 
1289     case TR_KEY_start_added_torrents:
1290         tr_sessionSetPaused(tr, !gtr_pref_flag_get(key));
1291         break;
1292 
1293     case TR_KEY_trash_original_torrent_files:
1294         tr_sessionSetDeleteSource(tr, gtr_pref_flag_get(key));
1295         break;
1296 
1297     default:
1298         break;
1299     }
1300 }
1301 
update_model_once(gpointer gdata)1302 static gboolean update_model_once(gpointer gdata)
1303 {
1304     struct cbdata* data = gdata;
1305 
1306     /* update the torrent data in the model */
1307     gtr_core_update(data->core);
1308 
1309     /* refresh the main window's statusbar and toolbar buttons */
1310     if (data->wind != NULL)
1311     {
1312         gtr_window_refresh(data->wind);
1313     }
1314 
1315     /* update the actions */
1316     refresh_actions(data);
1317 
1318     /* update the status tray icon */
1319     if (data->icon != NULL)
1320     {
1321         gtr_icon_refresh(data->icon);
1322     }
1323 
1324     data->update_model_soon_tag = 0;
1325     return G_SOURCE_REMOVE;
1326 }
1327 
update_model_soon(gpointer gdata)1328 static void update_model_soon(gpointer gdata)
1329 {
1330     struct cbdata* data = gdata;
1331 
1332     if (data->update_model_soon_tag == 0)
1333     {
1334         data->update_model_soon_tag = gdk_threads_add_idle(update_model_once, data);
1335     }
1336 }
1337 
update_model_loop(gpointer gdata)1338 static gboolean update_model_loop(gpointer gdata)
1339 {
1340     gboolean const done = global_sigcount != 0;
1341 
1342     if (!done)
1343     {
1344         update_model_once(gdata);
1345     }
1346 
1347     return !done;
1348 }
1349 
show_about_dialog(GtkWindow * parent)1350 static void show_about_dialog(GtkWindow* parent)
1351 {
1352     char const* uri = "https://transmissionbt.com/";
1353     char const* authors[] =
1354     {
1355         "Charles Kerr (Backend; GTK+)",
1356         "Mitchell Livingston (Backend; OS X)",
1357         "Mike Gelfand",
1358         NULL
1359     };
1360 
1361     gtk_show_about_dialog(parent,
1362         "authors", authors,
1363         "comments", _("A fast and easy BitTorrent client"),
1364         "copyright", _("Copyright (c) The Transmission Project"),
1365         "logo-icon-name", MY_CONFIG_NAME,
1366         "name", g_get_application_name(),
1367         /* Translators: translate "translator-credits" as your name
1368            to have it appear in the credits in the "About"
1369            dialog */
1370         "translator-credits", _("translator-credits"),
1371         "version", LONG_VERSION_STRING,
1372         "website", uri,
1373         "website-label", uri,
1374 #ifdef SHOW_LICENSE
1375         "license", LICENSE,
1376         "wrap-license", TRUE,
1377 #endif
1378         NULL);
1379 }
1380 
append_id_to_benc_list(GtkTreeModel * m,GtkTreePath * path UNUSED,GtkTreeIter * iter,gpointer list)1381 static void append_id_to_benc_list(GtkTreeModel* m, GtkTreePath* path UNUSED, GtkTreeIter* iter, gpointer list)
1382 {
1383     tr_torrent* tor = NULL;
1384     gtk_tree_model_get(m, iter, MC_TORRENT, &tor, -1);
1385     tr_variantListAddInt(list, tr_torrentId(tor));
1386 }
1387 
call_rpc_for_selected_torrents(struct cbdata * data,char const * method)1388 static gboolean call_rpc_for_selected_torrents(struct cbdata* data, char const* method)
1389 {
1390     tr_variant top;
1391     tr_variant* args;
1392     tr_variant* ids;
1393     gboolean invoked = FALSE;
1394     GtkTreeSelection* s = data->sel;
1395     tr_session* session = gtr_core_session(data->core);
1396 
1397     tr_variantInitDict(&top, 2);
1398     tr_variantDictAddStr(&top, TR_KEY_method, method);
1399     args = tr_variantDictAddDict(&top, TR_KEY_arguments, 1);
1400     ids = tr_variantDictAddList(args, TR_KEY_ids, 0);
1401     gtk_tree_selection_selected_foreach(s, append_id_to_benc_list, ids);
1402 
1403     if (tr_variantListSize(ids) != 0)
1404     {
1405         tr_rpc_request_exec_json(session, &top, NULL, NULL);
1406         invoked = TRUE;
1407     }
1408 
1409     tr_variantFree(&top);
1410     return invoked;
1411 }
1412 
open_folder_foreach(GtkTreeModel * model,GtkTreePath * path UNUSED,GtkTreeIter * iter,gpointer core)1413 static void open_folder_foreach(GtkTreeModel* model, GtkTreePath* path UNUSED, GtkTreeIter* iter, gpointer core)
1414 {
1415     int id;
1416     gtk_tree_model_get(model, iter, MC_TORRENT_ID, &id, -1);
1417     gtr_core_open_folder(core, id);
1418 }
1419 
on_message_window_closed(void)1420 static gboolean on_message_window_closed(void)
1421 {
1422     gtr_action_set_toggled("toggle-message-log", FALSE);
1423     return FALSE;
1424 }
1425 
accumulate_selected_torrents(GtkTreeModel * model,GtkTreePath * path UNUSED,GtkTreeIter * iter,gpointer gdata)1426 static void accumulate_selected_torrents(GtkTreeModel* model, GtkTreePath* path UNUSED, GtkTreeIter* iter, gpointer gdata)
1427 {
1428     int id;
1429     GSList** data = gdata;
1430 
1431     gtk_tree_model_get(model, iter, MC_TORRENT_ID, &id, -1);
1432     *data = g_slist_append(*data, GINT_TO_POINTER(id));
1433 }
1434 
remove_selected(struct cbdata * data,gboolean delete_files)1435 static void remove_selected(struct cbdata* data, gboolean delete_files)
1436 {
1437     GSList* l = NULL;
1438 
1439     gtk_tree_selection_selected_foreach(data->sel, accumulate_selected_torrents, &l);
1440 
1441     if (l != NULL)
1442     {
1443         gtr_confirm_remove(data->wind, data->core, l, delete_files);
1444     }
1445 }
1446 
start_all_torrents(struct cbdata * data)1447 static void start_all_torrents(struct cbdata* data)
1448 {
1449     tr_session* session = gtr_core_session(data->core);
1450     tr_variant request;
1451 
1452     tr_variantInitDict(&request, 1);
1453     tr_variantDictAddStr(&request, TR_KEY_method, "torrent-start");
1454     tr_rpc_request_exec_json(session, &request, NULL, NULL);
1455     tr_variantFree(&request);
1456 }
1457 
pause_all_torrents(struct cbdata * data)1458 static void pause_all_torrents(struct cbdata* data)
1459 {
1460     tr_session* session = gtr_core_session(data->core);
1461     tr_variant request;
1462 
1463     tr_variantInitDict(&request, 1);
1464     tr_variantDictAddStr(&request, TR_KEY_method, "torrent-stop");
1465     tr_rpc_request_exec_json(session, &request, NULL, NULL);
1466     tr_variantFree(&request);
1467 }
1468 
get_first_selected_torrent(struct cbdata * data)1469 static tr_torrent* get_first_selected_torrent(struct cbdata* data)
1470 {
1471     tr_torrent* tor = NULL;
1472     GtkTreeModel* m;
1473     GList* l = gtk_tree_selection_get_selected_rows(data->sel, &m);
1474 
1475     if (l != NULL)
1476     {
1477         GtkTreePath* p = l->data;
1478         GtkTreeIter i;
1479 
1480         if (gtk_tree_model_get_iter(m, &i, p))
1481         {
1482             gtk_tree_model_get(m, &i, MC_TORRENT, &tor, -1);
1483         }
1484     }
1485 
1486     g_list_foreach(l, (GFunc)(GCallback)gtk_tree_path_free, NULL);
1487     g_list_free(l);
1488     return tor;
1489 }
1490 
copy_magnet_link_to_clipboard(GtkWidget * w,tr_torrent * tor)1491 static void copy_magnet_link_to_clipboard(GtkWidget* w, tr_torrent* tor)
1492 {
1493     char* magnet = tr_torrentGetMagnetLink(tor);
1494     GdkDisplay* display = gtk_widget_get_display(w);
1495     GdkAtom selection;
1496     GtkClipboard* clipboard;
1497 
1498     /* this is The Right Thing for copy/paste... */
1499     selection = GDK_SELECTION_CLIPBOARD;
1500     clipboard = gtk_clipboard_get_for_display(display, selection);
1501     gtk_clipboard_set_text(clipboard, magnet, -1);
1502 
1503     /* ...but people using plain ol' X need this instead */
1504     selection = GDK_SELECTION_PRIMARY;
1505     clipboard = gtk_clipboard_get_for_display(display, selection);
1506     gtk_clipboard_set_text(clipboard, magnet, -1);
1507 
1508     /* cleanup */
1509     tr_free(magnet);
1510 }
1511 
gtr_actions_handler(char const * action_name,gpointer user_data)1512 void gtr_actions_handler(char const* action_name, gpointer user_data)
1513 {
1514     gboolean changed = FALSE;
1515     struct cbdata* data = user_data;
1516 
1517     if (g_strcmp0(action_name, "open-torrent-from-url") == 0)
1518     {
1519         GtkWidget* w = gtr_torrent_open_from_url_dialog_new(data->wind, data->core);
1520         gtk_widget_show(w);
1521     }
1522     else if (g_strcmp0(action_name, "open-torrent-menu") == 0 || g_strcmp0(action_name, "open-torrent-toolbar") == 0)
1523     {
1524         GtkWidget* w = gtr_torrent_open_from_file_dialog_new(data->wind, data->core);
1525         gtk_widget_show(w);
1526     }
1527     else if (g_strcmp0(action_name, "show-stats") == 0)
1528     {
1529         GtkWidget* dialog = gtr_stats_dialog_new(data->wind, data->core);
1530         gtk_widget_show(dialog);
1531     }
1532     else if (g_strcmp0(action_name, "donate") == 0)
1533     {
1534         gtr_open_uri("https://transmissionbt.com/donate/");
1535     }
1536     else if (g_strcmp0(action_name, "pause-all-torrents") == 0)
1537     {
1538         pause_all_torrents(data);
1539     }
1540     else if (g_strcmp0(action_name, "start-all-torrents") == 0)
1541     {
1542         start_all_torrents(data);
1543     }
1544     else if (g_strcmp0(action_name, "copy-magnet-link-to-clipboard") == 0)
1545     {
1546         tr_torrent* tor = get_first_selected_torrent(data);
1547 
1548         if (tor != NULL)
1549         {
1550             copy_magnet_link_to_clipboard(GTK_WIDGET(data->wind), tor);
1551         }
1552     }
1553     else if (g_strcmp0(action_name, "relocate-torrent") == 0)
1554     {
1555         GSList* ids = get_selected_torrent_ids(data);
1556 
1557         if (ids != NULL)
1558         {
1559             GtkWindow* parent = data->wind;
1560             GtkWidget* w = gtr_relocate_dialog_new(parent, data->core, ids);
1561             gtk_widget_show(w);
1562         }
1563     }
1564     else if (g_strcmp0(action_name, "torrent-start") == 0 || g_strcmp0(action_name, "torrent-start-now") == 0 ||
1565         g_strcmp0(action_name, "torrent-stop") == 0 || g_strcmp0(action_name, "torrent-reannounce") == 0 ||
1566         g_strcmp0(action_name, "torrent-verify") == 0 || g_strcmp0(action_name, "queue-move-top") == 0 ||
1567         g_strcmp0(action_name, "queue-move-up") == 0 || g_strcmp0(action_name, "queue-move-down") == 0 ||
1568         g_strcmp0(action_name, "queue-move-bottom") == 0)
1569     {
1570         changed |= call_rpc_for_selected_torrents(data, action_name);
1571     }
1572     else if (g_strcmp0(action_name, "open-torrent-folder") == 0)
1573     {
1574         gtk_tree_selection_selected_foreach(data->sel, open_folder_foreach, data->core);
1575     }
1576     else if (g_strcmp0(action_name, "show-torrent-properties") == 0)
1577     {
1578         show_details_dialog_for_selected_torrents(data);
1579     }
1580     else if (g_strcmp0(action_name, "new-torrent") == 0)
1581     {
1582         GtkWidget* w = gtr_torrent_creation_dialog_new(data->wind, data->core);
1583         gtk_widget_show(w);
1584     }
1585     else if (g_strcmp0(action_name, "remove-torrent") == 0)
1586     {
1587         remove_selected(data, FALSE);
1588     }
1589     else if (g_strcmp0(action_name, "delete-torrent") == 0)
1590     {
1591         remove_selected(data, TRUE);
1592     }
1593     else if (g_strcmp0(action_name, "quit") == 0)
1594     {
1595         on_app_exit(data);
1596     }
1597     else if (g_strcmp0(action_name, "select-all") == 0)
1598     {
1599         gtk_tree_selection_select_all(data->sel);
1600     }
1601     else if (g_strcmp0(action_name, "deselect-all") == 0)
1602     {
1603         gtk_tree_selection_unselect_all(data->sel);
1604     }
1605     else if (g_strcmp0(action_name, "edit-preferences") == 0)
1606     {
1607         if (data->prefs == NULL)
1608         {
1609             data->prefs = gtr_prefs_dialog_new(data->wind, G_OBJECT(data->core));
1610             g_signal_connect(data->prefs, "destroy", G_CALLBACK(gtk_widget_destroyed), &data->prefs);
1611         }
1612 
1613         gtr_window_present(GTK_WINDOW(data->prefs));
1614     }
1615     else if (g_strcmp0(action_name, "toggle-message-log") == 0)
1616     {
1617         if (data->msgwin == NULL)
1618         {
1619             GtkWidget* win = gtr_message_log_window_new(data->wind, data->core);
1620             g_signal_connect(win, "destroy", G_CALLBACK(on_message_window_closed), NULL);
1621             data->msgwin = win;
1622         }
1623         else
1624         {
1625             gtr_action_set_toggled("toggle-message-log", FALSE);
1626             gtk_widget_destroy(data->msgwin);
1627             data->msgwin = NULL;
1628         }
1629     }
1630     else if (g_strcmp0(action_name, "show-about-dialog") == 0)
1631     {
1632         show_about_dialog(data->wind);
1633     }
1634     else if (g_strcmp0(action_name, "help") == 0)
1635     {
1636         gtr_open_uri(gtr_get_help_uri());
1637     }
1638     else if (g_strcmp0(action_name, "toggle-main-window") == 0)
1639     {
1640         toggleMainWindow(data);
1641     }
1642     else if (g_strcmp0(action_name, "present-main-window") == 0)
1643     {
1644         presentMainWindow(data);
1645     }
1646     else
1647     {
1648         g_error("Unhandled action: %s", action_name);
1649     }
1650 
1651     if (changed)
1652     {
1653         update_model_soon(data);
1654     }
1655 }
1656