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