1 /*
2 * This file Copyright (C) 2007-2014 Mnemosyne LLC
3 *
4 * It may be used under the GNU GPL versions 2 or 3
5 * or any future license endorsed by Mnemosyne LLC.
6 *
7 */
8
9 #include <limits.h> /* INT_MAX */
10 #include <stddef.h>
11 #include <stdio.h> /* sscanf() */
12 #include <stdlib.h> /* abort() */
13 #include <glib/gi18n.h>
14 #include <gtk/gtk.h>
15
16 #include <libtransmission/transmission.h>
17 #include <libtransmission/utils.h> /* tr_free */
18
19 #include "actions.h"
20 #include "conf.h"
21 #include "details.h"
22 #include "favicon.h" /* gtr_get_favicon() */
23 #include "file-list.h"
24 #include "hig.h"
25 #include "tr-prefs.h"
26 #include "util.h"
27
28 static GQuark ARG_KEY = 0;
29 static GQuark DETAILS_KEY = 0;
30 static GQuark TORRENT_ID_KEY = 0;
31 static GQuark TEXT_BUFFER_KEY = 0;
32 static GQuark URL_ENTRY_KEY = 0;
33
34 struct DetailsImpl
35 {
36 GtkWidget* dialog;
37
38 GtkWidget* honor_limits_check;
39 GtkWidget* up_limited_check;
40 GtkWidget* up_limit_sping;
41 GtkWidget* down_limited_check;
42 GtkWidget* down_limit_spin;
43 GtkWidget* bandwidth_combo;
44
45 GtkWidget* ratio_combo;
46 GtkWidget* ratio_spin;
47 GtkWidget* idle_combo;
48 GtkWidget* idle_spin;
49 GtkWidget* max_peers_spin;
50
51 gulong honor_limits_check_tag;
52 gulong up_limited_check_tag;
53 gulong down_limited_check_tag;
54 gulong down_limit_spin_tag;
55 gulong up_limit_spin_tag;
56 gulong bandwidth_combo_tag;
57 gulong ratio_combo_tag;
58 gulong ratio_spin_tag;
59 gulong idle_combo_tag;
60 gulong idle_spin_tag;
61 gulong max_peers_spin_tag;
62
63 GtkWidget* size_lb;
64 GtkWidget* state_lb;
65 GtkWidget* have_lb;
66 GtkWidget* dl_lb;
67 GtkWidget* ul_lb;
68 GtkWidget* error_lb;
69 GtkWidget* date_started_lb;
70 GtkWidget* eta_lb;
71 GtkWidget* last_activity_lb;
72
73 GtkWidget* hash_lb;
74 GtkWidget* privacy_lb;
75 GtkWidget* origin_lb;
76 GtkWidget* destination_lb;
77 GtkTextBuffer* comment_buffer;
78
79 GHashTable* peer_hash;
80 GHashTable* webseed_hash;
81 GtkListStore* peer_store;
82 GtkListStore* webseed_store;
83 GtkWidget* webseed_view;
84 GtkWidget* peer_view;
85 GtkWidget* more_peer_details_check;
86
87 GtkListStore* tracker_store;
88 GHashTable* tracker_hash;
89 GtkTreeModel* trackers_filtered;
90 GtkWidget* add_tracker_button;
91 GtkWidget* edit_trackers_button;
92 GtkWidget* remove_tracker_button;
93 GtkWidget* tracker_view;
94 GtkWidget* scrape_check;
95 GtkWidget* all_check;
96
97 GtkWidget* file_list;
98 GtkWidget* file_label;
99
100 GSList* ids;
101 TrCore* core;
102 guint periodic_refresh_tag;
103
104 GString* gstr;
105 };
106
getTorrents(struct DetailsImpl * d,int * setmeCount)107 static tr_torrent** getTorrents(struct DetailsImpl* d, int* setmeCount)
108 {
109 int torrentCount = 0;
110 int const n = g_slist_length(d->ids);
111 tr_torrent** torrents = g_new(tr_torrent*, n);
112
113 for (GSList* l = d->ids; l != NULL; l = l->next)
114 {
115 if ((torrents[torrentCount] = gtr_core_find_torrent(d->core, GPOINTER_TO_INT(l->data))) != NULL)
116 {
117 ++torrentCount;
118 }
119 }
120
121 *setmeCount = torrentCount;
122 return torrents;
123 }
124
125 /****
126 *****
127 ***** OPTIONS TAB
128 *****
129 ****/
130
set_togglebutton_if_different(GtkWidget * w,gulong tag,gboolean value)131 static void set_togglebutton_if_different(GtkWidget* w, gulong tag, gboolean value)
132 {
133 GtkToggleButton* toggle = GTK_TOGGLE_BUTTON(w);
134 gboolean const currentValue = gtk_toggle_button_get_active(toggle);
135
136 if (currentValue != value)
137 {
138 g_signal_handler_block(toggle, tag);
139 gtk_toggle_button_set_active(toggle, value);
140 g_signal_handler_unblock(toggle, tag);
141 }
142 }
143
set_int_spin_if_different(GtkWidget * w,gulong tag,int value)144 static void set_int_spin_if_different(GtkWidget* w, gulong tag, int value)
145 {
146 GtkSpinButton* spin = GTK_SPIN_BUTTON(w);
147 int const currentValue = gtk_spin_button_get_value_as_int(spin);
148
149 if (currentValue != value)
150 {
151 g_signal_handler_block(spin, tag);
152 gtk_spin_button_set_value(spin, value);
153 g_signal_handler_unblock(spin, tag);
154 }
155 }
156
set_double_spin_if_different(GtkWidget * w,gulong tag,double value)157 static void set_double_spin_if_different(GtkWidget* w, gulong tag, double value)
158 {
159 GtkSpinButton* spin = GTK_SPIN_BUTTON(w);
160 double const currentValue = gtk_spin_button_get_value(spin);
161
162 if ((int)(currentValue * 100) != (int)(value * 100))
163 {
164 g_signal_handler_block(spin, tag);
165 gtk_spin_button_set_value(spin, value);
166 g_signal_handler_unblock(spin, tag);
167 }
168 }
169
unset_combo(GtkWidget * w,gulong tag)170 static void unset_combo(GtkWidget* w, gulong tag)
171 {
172 GtkComboBox* combobox = GTK_COMBO_BOX(w);
173
174 g_signal_handler_block(combobox, tag);
175 gtk_combo_box_set_active(combobox, -1);
176 g_signal_handler_unblock(combobox, tag);
177 }
178
refreshOptions(struct DetailsImpl * di,tr_torrent ** torrents,int n)179 static void refreshOptions(struct DetailsImpl* di, tr_torrent** torrents, int n)
180 {
181 /***
182 **** Options Page
183 ***/
184
185 /* honor_limits_check */
186 if (n != 0)
187 {
188 bool const baseline = tr_torrentUsesSessionLimits(torrents[0]);
189 bool is_uniform = true;
190
191 for (int i = 1; is_uniform && i < n; ++i)
192 {
193 is_uniform = baseline == tr_torrentUsesSessionLimits(torrents[i]);
194 }
195
196 if (is_uniform)
197 {
198 set_togglebutton_if_different(di->honor_limits_check, di->honor_limits_check_tag, baseline);
199 }
200 }
201
202 /* down_limited_check */
203 if (n != 0)
204 {
205 bool const baseline = tr_torrentUsesSpeedLimit(torrents[0], TR_DOWN);
206 bool is_uniform = true;
207
208 for (int i = 1; is_uniform && i < n; ++i)
209 {
210 is_uniform = baseline == tr_torrentUsesSpeedLimit(torrents[i], TR_DOWN);
211 }
212
213 if (is_uniform)
214 {
215 set_togglebutton_if_different(di->down_limited_check, di->down_limited_check_tag, baseline);
216 }
217 }
218
219 /* down_limit_spin */
220 if (n != 0)
221 {
222 unsigned int const baseline = tr_torrentGetSpeedLimit_KBps(torrents[0], TR_DOWN);
223 bool is_uniform = true;
224
225 for (int i = 1; is_uniform && i < n; ++i)
226 {
227 is_uniform = baseline == tr_torrentGetSpeedLimit_KBps(torrents[i], TR_DOWN);
228 }
229
230 if (is_uniform)
231 {
232 set_int_spin_if_different(di->down_limit_spin, di->down_limit_spin_tag, baseline);
233 }
234 }
235
236 /* up_limited_check */
237 if (n != 0)
238 {
239 bool const baseline = tr_torrentUsesSpeedLimit(torrents[0], TR_UP);
240 bool is_uniform = true;
241
242 for (int i = 1; is_uniform && i < n; ++i)
243 {
244 is_uniform = baseline == tr_torrentUsesSpeedLimit(torrents[i], TR_UP);
245 }
246
247 if (is_uniform)
248 {
249 set_togglebutton_if_different(di->up_limited_check, di->up_limited_check_tag, baseline);
250 }
251 }
252
253 /* up_limit_sping */
254 if (n != 0)
255 {
256 unsigned int const baseline = tr_torrentGetSpeedLimit_KBps(torrents[0], TR_UP);
257 bool is_uniform = true;
258
259 for (int i = 1; is_uniform && i < n; ++i)
260 {
261 is_uniform = baseline == tr_torrentGetSpeedLimit_KBps(torrents[i], TR_UP);
262 }
263
264 if (is_uniform)
265 {
266 set_int_spin_if_different(di->up_limit_sping, di->up_limit_spin_tag, baseline);
267 }
268 }
269
270 /* bandwidth_combo */
271 if (n != 0)
272 {
273 int const baseline = tr_torrentGetPriority(torrents[0]);
274 bool is_uniform = true;
275
276 for (int i = 1; is_uniform && i < n; ++i)
277 {
278 is_uniform = baseline == tr_torrentGetPriority(torrents[i]);
279 }
280
281 if (is_uniform)
282 {
283 GtkWidget* w = di->bandwidth_combo;
284 g_signal_handler_block(w, di->bandwidth_combo_tag);
285 gtr_priority_combo_set_value(GTK_COMBO_BOX(w), baseline);
286 g_signal_handler_unblock(w, di->bandwidth_combo_tag);
287 }
288 else
289 {
290 unset_combo(di->bandwidth_combo, di->bandwidth_combo_tag);
291 }
292 }
293
294 /* ratio_combo */
295 if (n != 0)
296 {
297 int const baseline = tr_torrentGetRatioMode(torrents[0]);
298 bool is_uniform = true;
299
300 for (int i = 1; is_uniform && i < n; ++i)
301 {
302 is_uniform = baseline == (int)tr_torrentGetRatioMode(torrents[i]);
303 }
304
305 if (is_uniform)
306 {
307 GtkWidget* w = di->ratio_combo;
308 g_signal_handler_block(w, di->ratio_combo_tag);
309 gtr_combo_box_set_active_enum(GTK_COMBO_BOX(w), baseline);
310 gtr_widget_set_visible(di->ratio_spin, baseline == TR_RATIOLIMIT_SINGLE);
311 g_signal_handler_unblock(w, di->ratio_combo_tag);
312 }
313 }
314
315 /* ratio_spin */
316 if (n != 0)
317 {
318 double const baseline = tr_torrentGetRatioLimit(torrents[0]);
319 set_double_spin_if_different(di->ratio_spin, di->ratio_spin_tag, baseline);
320 }
321
322 /* idle_combo */
323 if (n != 0)
324 {
325 int const baseline = tr_torrentGetIdleMode(torrents[0]);
326 bool is_uniform = true;
327
328 for (int i = 1; is_uniform && i < n; ++i)
329 {
330 is_uniform = baseline == (int)tr_torrentGetIdleMode(torrents[i]);
331 }
332
333 if (is_uniform)
334 {
335 GtkWidget* w = di->idle_combo;
336 g_signal_handler_block(w, di->idle_combo_tag);
337 gtr_combo_box_set_active_enum(GTK_COMBO_BOX(w), baseline);
338 gtr_widget_set_visible(di->idle_spin, baseline == TR_IDLELIMIT_SINGLE);
339 g_signal_handler_unblock(w, di->idle_combo_tag);
340 }
341 }
342
343 /* idle_spin */
344 if (n != 0)
345 {
346 int const baseline = tr_torrentGetIdleLimit(torrents[0]);
347 set_int_spin_if_different(di->idle_spin, di->idle_spin_tag, baseline);
348 }
349
350 /* max_peers_spin */
351 if (n != 0)
352 {
353 int const baseline = tr_torrentGetPeerLimit(torrents[0]);
354 set_int_spin_if_different(di->max_peers_spin, di->max_peers_spin_tag, baseline);
355 }
356 }
357
torrent_set_bool(struct DetailsImpl * di,tr_quark const key,gboolean value)358 static void torrent_set_bool(struct DetailsImpl* di, tr_quark const key, gboolean value)
359 {
360 tr_variant top;
361 tr_variant* args;
362 tr_variant* ids;
363
364 tr_variantInitDict(&top, 2);
365 tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set");
366 args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
367 tr_variantDictAddBool(args, key, value);
368 ids = tr_variantDictAddList(args, TR_KEY_ids, g_slist_length(di->ids));
369
370 for (GSList* l = di->ids; l != NULL; l = l->next)
371 {
372 tr_variantListAddInt(ids, GPOINTER_TO_INT(l->data));
373 }
374
375 gtr_core_exec(di->core, &top);
376 tr_variantFree(&top);
377 }
378
torrent_set_int(struct DetailsImpl * di,tr_quark const key,int value)379 static void torrent_set_int(struct DetailsImpl* di, tr_quark const key, int value)
380 {
381 tr_variant top;
382 tr_variant* args;
383 tr_variant* ids;
384
385 tr_variantInitDict(&top, 2);
386 tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set");
387 args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
388 tr_variantDictAddInt(args, key, value);
389 ids = tr_variantDictAddList(args, TR_KEY_ids, g_slist_length(di->ids));
390
391 for (GSList* l = di->ids; l != NULL; l = l->next)
392 {
393 tr_variantListAddInt(ids, GPOINTER_TO_INT(l->data));
394 }
395
396 gtr_core_exec(di->core, &top);
397 tr_variantFree(&top);
398 }
399
torrent_set_real(struct DetailsImpl * di,tr_quark const key,double value)400 static void torrent_set_real(struct DetailsImpl* di, tr_quark const key, double value)
401 {
402 tr_variant top;
403 tr_variant* args;
404 tr_variant* ids;
405
406 tr_variantInitDict(&top, 2);
407 tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set");
408 args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
409 tr_variantDictAddReal(args, key, value);
410 ids = tr_variantDictAddList(args, TR_KEY_ids, g_slist_length(di->ids));
411
412 for (GSList* l = di->ids; l != NULL; l = l->next)
413 {
414 tr_variantListAddInt(ids, GPOINTER_TO_INT(l->data));
415 }
416
417 gtr_core_exec(di->core, &top);
418 tr_variantFree(&top);
419 }
420
up_speed_toggled_cb(GtkToggleButton * tb,gpointer d)421 static void up_speed_toggled_cb(GtkToggleButton* tb, gpointer d)
422 {
423 torrent_set_bool(d, TR_KEY_uploadLimited, gtk_toggle_button_get_active(tb));
424 }
425
down_speed_toggled_cb(GtkToggleButton * tb,gpointer d)426 static void down_speed_toggled_cb(GtkToggleButton* tb, gpointer d)
427 {
428 torrent_set_bool(d, TR_KEY_downloadLimited, gtk_toggle_button_get_active(tb));
429 }
430
global_speed_toggled_cb(GtkToggleButton * tb,gpointer d)431 static void global_speed_toggled_cb(GtkToggleButton* tb, gpointer d)
432 {
433 torrent_set_bool(d, TR_KEY_honorsSessionLimits, gtk_toggle_button_get_active(tb));
434 }
435
up_speed_spun_cb(GtkSpinButton * s,struct DetailsImpl * di)436 static void up_speed_spun_cb(GtkSpinButton* s, struct DetailsImpl* di)
437 {
438 torrent_set_int(di, TR_KEY_uploadLimit, gtk_spin_button_get_value_as_int(s));
439 }
440
down_speed_spun_cb(GtkSpinButton * s,struct DetailsImpl * di)441 static void down_speed_spun_cb(GtkSpinButton* s, struct DetailsImpl* di)
442 {
443 torrent_set_int(di, TR_KEY_downloadLimit, gtk_spin_button_get_value_as_int(s));
444 }
445
idle_spun_cb(GtkSpinButton * s,struct DetailsImpl * di)446 static void idle_spun_cb(GtkSpinButton* s, struct DetailsImpl* di)
447 {
448 torrent_set_int(di, TR_KEY_seedIdleLimit, gtk_spin_button_get_value_as_int(s));
449 }
450
ratio_spun_cb(GtkSpinButton * s,struct DetailsImpl * di)451 static void ratio_spun_cb(GtkSpinButton* s, struct DetailsImpl* di)
452 {
453 torrent_set_real(di, TR_KEY_seedRatioLimit, gtk_spin_button_get_value(s));
454 }
455
max_peers_spun_cb(GtkSpinButton * s,struct DetailsImpl * di)456 static void max_peers_spun_cb(GtkSpinButton* s, struct DetailsImpl* di)
457 {
458 torrent_set_int(di, TR_KEY_peer_limit, gtk_spin_button_get_value(s));
459 }
460
onPriorityChanged(GtkComboBox * combo_box,struct DetailsImpl * di)461 static void onPriorityChanged(GtkComboBox* combo_box, struct DetailsImpl* di)
462 {
463 tr_priority_t const priority = gtr_priority_combo_get_value(combo_box);
464 torrent_set_int(di, TR_KEY_bandwidthPriority, priority);
465 }
466
new_priority_combo(struct DetailsImpl * di)467 static GtkWidget* new_priority_combo(struct DetailsImpl* di)
468 {
469 GtkWidget* w = gtr_priority_combo_new();
470 di->bandwidth_combo_tag = g_signal_connect(w, "changed", G_CALLBACK(onPriorityChanged), di);
471 return w;
472 }
473
474 static void refresh(struct DetailsImpl* di);
475
onComboEnumChanged(GtkComboBox * combo_box,struct DetailsImpl * di)476 static void onComboEnumChanged(GtkComboBox* combo_box, struct DetailsImpl* di)
477 {
478 tr_quark const key = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(combo_box), ARG_KEY));
479 torrent_set_int(di, key, gtr_combo_box_get_active_enum(combo_box));
480 refresh(di);
481 }
482
ratio_combo_new(void)483 static GtkWidget* ratio_combo_new(void)
484 {
485 GtkWidget* w = gtr_combo_box_new_enum(
486 _("Use global settings"), TR_RATIOLIMIT_GLOBAL,
487 _("Seed regardless of ratio"), TR_RATIOLIMIT_UNLIMITED,
488 _("Stop seeding at ratio:"), TR_RATIOLIMIT_SINGLE,
489 NULL);
490 g_object_set_qdata(G_OBJECT(w), ARG_KEY, GINT_TO_POINTER(TR_KEY_seedRatioMode));
491 return w;
492 }
493
idle_combo_new(void)494 static GtkWidget* idle_combo_new(void)
495 {
496 GtkWidget* w = gtr_combo_box_new_enum(
497 _("Use global settings"), TR_IDLELIMIT_GLOBAL,
498 _("Seed regardless of activity"), TR_IDLELIMIT_UNLIMITED,
499 _("Stop seeding if idle for N minutes:"), TR_IDLELIMIT_SINGLE,
500 NULL);
501 g_object_set_qdata(G_OBJECT(w), ARG_KEY, GINT_TO_POINTER(TR_KEY_seedIdleMode));
502 return w;
503 }
504
options_page_new(struct DetailsImpl * d)505 static GtkWidget* options_page_new(struct DetailsImpl* d)
506 {
507 guint row;
508 gulong tag;
509 char buf[128];
510 GtkWidget* t;
511 GtkWidget* w;
512 GtkWidget* tb;
513 GtkWidget* h;
514
515 row = 0;
516 t = hig_workarea_create();
517 hig_workarea_add_section_title(t, &row, _("Speed"));
518
519 tb = hig_workarea_add_wide_checkbutton(t, &row, _("Honor global _limits"), 0);
520 d->honor_limits_check = tb;
521 tag = g_signal_connect(tb, "toggled", G_CALLBACK(global_speed_toggled_cb), d);
522 d->honor_limits_check_tag = tag;
523
524 g_snprintf(buf, sizeof(buf), _("Limit _download speed (%s):"), _(speed_K_str));
525 tb = gtk_check_button_new_with_mnemonic(buf);
526 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(tb), FALSE);
527 d->down_limited_check = tb;
528 tag = g_signal_connect(tb, "toggled", G_CALLBACK(down_speed_toggled_cb), d);
529 d->down_limited_check_tag = tag;
530
531 w = gtk_spin_button_new_with_range(0, INT_MAX, 5);
532 tag = g_signal_connect(w, "value-changed", G_CALLBACK(down_speed_spun_cb), d);
533 d->down_limit_spin_tag = tag;
534 hig_workarea_add_row_w(t, &row, tb, w, NULL);
535 d->down_limit_spin = w;
536
537 g_snprintf(buf, sizeof(buf), _("Limit _upload speed (%s):"), _(speed_K_str));
538 tb = gtk_check_button_new_with_mnemonic(buf);
539 d->up_limited_check = tb;
540 tag = g_signal_connect(tb, "toggled", G_CALLBACK(up_speed_toggled_cb), d);
541 d->up_limited_check_tag = tag;
542
543 w = gtk_spin_button_new_with_range(0, INT_MAX, 5);
544 tag = g_signal_connect(w, "value-changed", G_CALLBACK(up_speed_spun_cb), d);
545 d->up_limit_spin_tag = tag;
546 hig_workarea_add_row_w(t, &row, tb, w, NULL);
547 d->up_limit_sping = w;
548
549 w = new_priority_combo(d);
550 hig_workarea_add_row(t, &row, _("Torrent _priority:"), w, NULL);
551 d->bandwidth_combo = w;
552
553 hig_workarea_add_section_divider(t, &row);
554 hig_workarea_add_section_title(t, &row, _("Seeding Limits"));
555
556 h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD);
557 w = d->ratio_combo = ratio_combo_new();
558 d->ratio_combo_tag = g_signal_connect(w, "changed", G_CALLBACK(onComboEnumChanged), d);
559 gtk_box_pack_start(GTK_BOX(h), w, TRUE, TRUE, 0);
560 w = d->ratio_spin = gtk_spin_button_new_with_range(0, 1000, .05);
561 gtk_entry_set_width_chars(GTK_ENTRY(w), 7);
562 d->ratio_spin_tag = g_signal_connect(w, "value-changed", G_CALLBACK(ratio_spun_cb), d);
563 gtk_box_pack_start(GTK_BOX(h), w, FALSE, FALSE, 0);
564 hig_workarea_add_row(t, &row, _("_Ratio:"), h, NULL);
565
566 h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD);
567 w = d->idle_combo = idle_combo_new();
568 d->idle_combo_tag = g_signal_connect(w, "changed", G_CALLBACK(onComboEnumChanged), d);
569 gtk_box_pack_start(GTK_BOX(h), w, TRUE, TRUE, 0);
570 w = d->idle_spin = gtk_spin_button_new_with_range(1, 40320, 5);
571 d->idle_spin_tag = g_signal_connect(w, "value-changed", G_CALLBACK(idle_spun_cb), d);
572 gtk_box_pack_start(GTK_BOX(h), w, FALSE, FALSE, 0);
573 hig_workarea_add_row(t, &row, _("_Idle:"), h, NULL);
574
575 hig_workarea_add_section_divider(t, &row);
576 hig_workarea_add_section_title(t, &row, _("Peer Connections"));
577
578 w = gtk_spin_button_new_with_range(1, 3000, 5);
579 hig_workarea_add_row(t, &row, _("_Maximum peers:"), w, w);
580 tag = g_signal_connect(w, "value-changed", G_CALLBACK(max_peers_spun_cb), d);
581 d->max_peers_spin = w;
582 d->max_peers_spin_tag = tag;
583
584 return t;
585 }
586
587 /****
588 *****
589 ***** INFO TAB
590 *****
591 ****/
592
activityString(int activity,bool finished)593 static char const* activityString(int activity, bool finished)
594 {
595 switch (activity)
596 {
597 case TR_STATUS_CHECK_WAIT:
598 return _("Queued for verification");
599
600 case TR_STATUS_CHECK:
601 return _("Verifying local data");
602
603 case TR_STATUS_DOWNLOAD_WAIT:
604 return _("Queued for download");
605
606 case TR_STATUS_DOWNLOAD:
607 return C_("Verb", "Downloading");
608
609 case TR_STATUS_SEED_WAIT:
610 return _("Queued for seeding");
611
612 case TR_STATUS_SEED:
613 return C_("Verb", "Seeding");
614
615 case TR_STATUS_STOPPED:
616 return finished ? _("Finished") : _("Paused");
617 }
618
619 return "";
620 }
621
622 /* Only call gtk_text_buffer_set_text () if the new text differs from the old.
623 * This way if the user has text selected, refreshing won't deselect it */
gtr_text_buffer_set_text(GtkTextBuffer * b,char const * str)624 static void gtr_text_buffer_set_text(GtkTextBuffer* b, char const* str)
625 {
626 char* old_str;
627 GtkTextIter start;
628 GtkTextIter end;
629
630 if (str == NULL)
631 {
632 str = "";
633 }
634
635 gtk_text_buffer_get_bounds(b, &start, &end);
636 old_str = gtk_text_buffer_get_text(b, &start, &end, FALSE);
637
638 if (old_str == NULL || g_strcmp0(old_str, str) != 0)
639 {
640 gtk_text_buffer_set_text(b, str, -1);
641 }
642
643 g_free(old_str);
644 }
645
get_short_date_string(time_t t)646 static char* get_short_date_string(time_t t)
647 {
648 char buf[64];
649 struct tm tm;
650
651 if (t == 0)
652 {
653 return g_strdup(_("N/A"));
654 }
655
656 tr_localtime_r(&t, &tm);
657 strftime(buf, sizeof(buf), "%d %b %Y", &tm);
658 return g_locale_to_utf8(buf, -1, NULL, NULL, NULL);
659 };
660
refreshInfo(struct DetailsImpl * di,tr_torrent ** torrents,int n)661 static void refreshInfo(struct DetailsImpl* di, tr_torrent** torrents, int n)
662 {
663 char const* str;
664 char const* mixed = _("Mixed");
665 char const* no_torrent = _("No Torrents Selected");
666 char const* stateString;
667 char buf[512];
668 uint64_t sizeWhenDone = 0;
669 tr_stat const** stats = g_new(tr_stat const*, n);
670 tr_info const** infos = g_new(tr_info const*, n);
671
672 for (int i = 0; i < n; ++i)
673 {
674 stats[i] = tr_torrentStatCached(torrents[i]);
675 infos[i] = tr_torrentInfo(torrents[i]);
676 }
677
678 /* privacy_lb */
679 if (n <= 0)
680 {
681 str = no_torrent;
682 }
683 else
684 {
685 bool const baseline = infos[0]->isPrivate;
686 bool is_uniform = true;
687
688 for (int i = 1; is_uniform && i < n; ++i)
689 {
690 is_uniform = baseline == infos[i]->isPrivate;
691 }
692
693 if (is_uniform)
694 {
695 str = baseline ? _("Private to this tracker -- DHT and PEX disabled") : _("Public torrent");
696 }
697 else
698 {
699 str = mixed;
700 }
701 }
702
703 gtr_label_set_text(GTK_LABEL(di->privacy_lb), str);
704
705 /* origin_lb */
706 if (n <= 0)
707 {
708 str = no_torrent;
709 }
710 else
711 {
712 char const* creator = infos[0]->creator != NULL ? infos[0]->creator : "";
713 time_t const date = infos[0]->dateCreated;
714 char* datestr = get_short_date_string(date);
715 gboolean mixed_creator = FALSE;
716 gboolean mixed_date = FALSE;
717
718 for (int i = 1; i < n; ++i)
719 {
720 mixed_creator |= g_strcmp0(creator, infos[i]->creator != NULL ? infos[i]->creator : "") != 0;
721 mixed_date |= date != infos[i]->dateCreated;
722 }
723
724 gboolean const empty_creator = tr_str_is_empty(creator);
725 gboolean const empty_date = date == 0;
726
727 if (mixed_date || mixed_creator)
728 {
729 str = mixed;
730 }
731 else if (empty_date && empty_creator)
732 {
733 str = _("N/A");
734 }
735 else
736 {
737 if (empty_date && !empty_creator)
738 {
739 g_snprintf(buf, sizeof(buf), _("Created by %1$s"), creator);
740 }
741 else if (empty_creator && !empty_date)
742 {
743 g_snprintf(buf, sizeof(buf), _("Created on %1$s"), datestr);
744 }
745 else
746 {
747 g_snprintf(buf, sizeof(buf), _("Created by %1$s on %2$s"), creator, datestr);
748 }
749
750 str = buf;
751 }
752
753 g_free(datestr);
754 }
755
756 gtr_label_set_text(GTK_LABEL(di->origin_lb), str);
757
758 /* comment_buffer */
759 if (n <= 0)
760 {
761 str = "";
762 }
763 else
764 {
765 char const* baseline = infos[0]->comment != NULL ? infos[0]->comment : "";
766 bool is_uniform = true;
767
768 for (int i = 1; is_uniform && i < n; ++i)
769 {
770 is_uniform = g_strcmp0(baseline, infos[i]->comment != NULL ? infos[i]->comment : "") == 0;
771 }
772
773 str = is_uniform ? baseline : mixed;
774 }
775
776 gtr_text_buffer_set_text(di->comment_buffer, str);
777
778 /* destination_lb */
779 if (n <= 0)
780 {
781 str = no_torrent;
782 }
783 else
784 {
785 char const* baseline = tr_torrentGetDownloadDir(torrents[0]);
786 bool is_uniform = true;
787
788 for (int i = 1; is_uniform && i < n; ++i)
789 {
790 is_uniform = g_strcmp0(baseline, tr_torrentGetDownloadDir(torrents[i])) == 0;
791 }
792
793 str = is_uniform ? baseline : mixed;
794 }
795
796 gtr_label_set_text(GTK_LABEL(di->destination_lb), str);
797
798 /* state_lb */
799 if (n <= 0)
800 {
801 str = no_torrent;
802 }
803 else
804 {
805 tr_torrent_activity const activity = stats[0]->activity;
806 bool is_uniform = true;
807 bool allFinished = stats[0]->finished;
808
809 for (int i = 1; is_uniform && i < n; ++i)
810 {
811 is_uniform = activity == stats[i]->activity;
812
813 if (!stats[i]->finished)
814 {
815 allFinished = false;
816 }
817 }
818
819 str = is_uniform ? activityString(activity, allFinished) : mixed;
820 }
821
822 stateString = str;
823 gtr_label_set_text(GTK_LABEL(di->state_lb), str);
824
825 /* date started */
826 if (n <= 0)
827 {
828 str = no_torrent;
829 }
830 else
831 {
832 time_t const baseline = stats[0]->startDate;
833 bool is_uniform = true;
834
835 for (int i = 1; is_uniform && i < n; ++i)
836 {
837 is_uniform = baseline == stats[i]->startDate;
838 }
839
840 if (!is_uniform)
841 {
842 str = mixed;
843 }
844 else if (baseline <= 0 || stats[0]->activity == TR_STATUS_STOPPED)
845 {
846 str = stateString;
847 }
848 else
849 {
850 str = tr_strltime(buf, time(NULL) - baseline, sizeof(buf));
851 }
852 }
853
854 gtr_label_set_text(GTK_LABEL(di->date_started_lb), str);
855
856 /* eta */
857 if (n <= 0)
858 {
859 str = no_torrent;
860 }
861 else
862 {
863 int const baseline = stats[0]->eta;
864 bool is_uniform = true;
865
866 for (int i = 1; is_uniform && i < n; ++i)
867 {
868 is_uniform = baseline == stats[i]->eta;
869 }
870
871 if (!is_uniform)
872 {
873 str = mixed;
874 }
875 else if (baseline < 0)
876 {
877 str = _("Unknown");
878 }
879 else
880 {
881 str = tr_strltime(buf, baseline, sizeof(buf));
882 }
883 }
884
885 gtr_label_set_text(GTK_LABEL(di->eta_lb), str);
886
887 /* size_lb */
888 {
889 char sizebuf[128];
890 uint64_t size = 0;
891 int pieces = 0;
892 int32_t pieceSize = 0;
893
894 for (int i = 0; i < n; ++i)
895 {
896 size += infos[i]->totalSize;
897 pieces += infos[i]->pieceCount;
898
899 if (pieceSize == 0)
900 {
901 pieceSize = infos[i]->pieceSize;
902 }
903 else if (pieceSize != (int)infos[i]->pieceSize)
904 {
905 pieceSize = -1;
906 }
907 }
908
909 tr_strlsize(sizebuf, size, sizeof(sizebuf));
910
911 if (size == 0)
912 {
913 str = "";
914 }
915 else if (pieceSize >= 0)
916 {
917 char piecebuf[128];
918 tr_formatter_mem_B(piecebuf, pieceSize, sizeof(piecebuf));
919 g_snprintf(buf, sizeof(buf), ngettext("%1$s (%2$'d piece @ %3$s)", "%1$s (%2$'d pieces @ %3$s)", pieces), sizebuf,
920 pieces, piecebuf);
921 str = buf;
922 }
923 else
924 {
925 g_snprintf(buf, sizeof(buf), ngettext("%1$s (%2$'d piece)", "%1$s (%2$'d pieces)", pieces), sizebuf, pieces);
926 str = buf;
927 }
928
929 gtr_label_set_text(GTK_LABEL(di->size_lb), str);
930 }
931
932 /* have_lb */
933 if (n <= 0)
934 {
935 str = no_torrent;
936 }
937 else
938 {
939 uint64_t leftUntilDone = 0;
940 uint64_t haveUnchecked = 0;
941 uint64_t haveValid = 0;
942 uint64_t available = 0;
943
944 for (int i = 0; i < n; ++i)
945 {
946 tr_stat const* st = stats[i];
947 haveUnchecked += st->haveUnchecked;
948 haveValid += st->haveValid;
949 sizeWhenDone += st->sizeWhenDone;
950 leftUntilDone += st->leftUntilDone;
951 available += st->sizeWhenDone - st->leftUntilDone + st->haveUnchecked + st->desiredAvailable;
952 }
953
954 {
955 char buf2[32];
956 char unver[64];
957 char total[64];
958 char avail[32];
959 double const d = sizeWhenDone != 0 ? (100.0 * available) / sizeWhenDone : 0;
960 double const ratio = 100.0 * (sizeWhenDone != 0 ? (haveValid + haveUnchecked) / (double)sizeWhenDone : 1);
961
962 tr_strlpercent(avail, d, sizeof(avail));
963 tr_strlpercent(buf2, ratio, sizeof(buf2));
964 tr_strlsize(total, haveUnchecked + haveValid, sizeof(total));
965 tr_strlsize(unver, haveUnchecked, sizeof(unver));
966
967 if (haveUnchecked == 0 && leftUntilDone == 0)
968 {
969 g_snprintf(buf, sizeof(buf), _("%1$s (%2$s%%)"), total, buf2);
970 }
971 else if (haveUnchecked == 0)
972 {
973 g_snprintf(buf, sizeof(buf), _("%1$s (%2$s%% of %3$s%% Available)"), total, buf2, avail);
974 }
975 else
976 {
977 g_snprintf(buf, sizeof(buf), _("%1$s (%2$s%% of %3$s%% Available); %4$s Unverified"), total, buf2, avail,
978 unver);
979 }
980
981 str = buf;
982 }
983 }
984
985 gtr_label_set_text(GTK_LABEL(di->have_lb), str);
986
987 /* dl_lb */
988 if (n <= 0)
989 {
990 str = no_torrent;
991 }
992 else
993 {
994 char dbuf[64];
995 char fbuf[64];
996 uint64_t d = 0;
997 uint64_t f = 0;
998
999 for (int i = 0; i < n; ++i)
1000 {
1001 d += stats[i]->downloadedEver;
1002 f += stats[i]->corruptEver;
1003 }
1004
1005 tr_strlsize(dbuf, d, sizeof(dbuf));
1006 tr_strlsize(fbuf, f, sizeof(fbuf));
1007
1008 if (f != 0)
1009 {
1010 g_snprintf(buf, sizeof(buf), _("%1$s (+%2$s corrupt)"), dbuf, fbuf);
1011 }
1012 else
1013 {
1014 tr_strlcpy(buf, dbuf, sizeof(buf));
1015 }
1016
1017 str = buf;
1018 }
1019
1020 gtr_label_set_text(GTK_LABEL(di->dl_lb), str);
1021
1022 /* ul_lb */
1023 if (n <= 0)
1024 {
1025 str = no_torrent;
1026 }
1027 else
1028 {
1029 char upstr[64];
1030 char ratiostr[64];
1031 uint64_t up = 0;
1032 uint64_t down = 0;
1033
1034 for (int i = 0; i < n; ++i)
1035 {
1036 up += stats[i]->uploadedEver;
1037 down += stats[i]->downloadedEver;
1038 }
1039
1040 tr_strlsize(upstr, up, sizeof(upstr));
1041 tr_strlratio(ratiostr, tr_getRatio(up, down), sizeof(ratiostr));
1042 g_snprintf(buf, sizeof(buf), _("%s (Ratio: %s)"), upstr, ratiostr);
1043 str = buf;
1044 }
1045
1046 gtr_label_set_text(GTK_LABEL(di->ul_lb), str);
1047
1048 /* hash_lb */
1049 if (n <= 0)
1050 {
1051 str = no_torrent;
1052 }
1053 else if (n == 1)
1054 {
1055 str = infos[0]->hashString;
1056 }
1057 else
1058 {
1059 str = mixed;
1060 }
1061
1062 gtr_label_set_text(GTK_LABEL(di->hash_lb), str);
1063
1064 /* error */
1065 if (n <= 0)
1066 {
1067 str = no_torrent;
1068 }
1069 else
1070 {
1071 char const* baseline = stats[0]->errorString;
1072 bool is_uniform = true;
1073
1074 for (int i = 1; is_uniform && i < n; ++i)
1075 {
1076 is_uniform = g_strcmp0(baseline, stats[i]->errorString) == 0;
1077 }
1078
1079 str = is_uniform ? baseline : mixed;
1080 }
1081
1082 if (tr_str_is_empty(str))
1083 {
1084 str = _("No errors");
1085 }
1086
1087 gtr_label_set_text(GTK_LABEL(di->error_lb), str);
1088
1089 /* activity date */
1090 if (n <= 0)
1091 {
1092 str = no_torrent;
1093 }
1094 else
1095 {
1096 time_t latest = 0;
1097
1098 for (int i = 0; i < n; ++i)
1099 {
1100 if (latest < stats[i]->activityDate)
1101 {
1102 latest = stats[i]->activityDate;
1103 }
1104 }
1105
1106 if (latest <= 0)
1107 {
1108 str = _("Never");
1109 }
1110 else
1111 {
1112 int const period = time(NULL) - latest;
1113
1114 if (period < 5)
1115 {
1116 tr_strlcpy(buf, _("Active now"), sizeof(buf));
1117 }
1118 else
1119 {
1120 char tbuf[128];
1121 tr_strltime(tbuf, period, sizeof(tbuf));
1122 g_snprintf(buf, sizeof(buf), _("%1$s ago"), tbuf);
1123 }
1124
1125 str = buf;
1126 }
1127 }
1128
1129 gtr_label_set_text(GTK_LABEL(di->last_activity_lb), str);
1130
1131 g_free(stats);
1132 g_free(infos);
1133 }
1134
info_page_new(struct DetailsImpl * di)1135 static GtkWidget* info_page_new(struct DetailsImpl* di)
1136 {
1137 guint row = 0;
1138 GtkTextBuffer* b;
1139 GtkWidget* l;
1140 GtkWidget* w;
1141 GtkWidget* fr;
1142 GtkWidget* sw;
1143 GtkWidget* t = hig_workarea_create();
1144
1145 hig_workarea_add_section_title(t, &row, _("Activity"));
1146
1147 /* size */
1148 l = di->size_lb = gtk_label_new(NULL);
1149 gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
1150 hig_workarea_add_row(t, &row, _("Torrent size:"), l, NULL);
1151
1152 /* have */
1153 l = di->have_lb = gtk_label_new(NULL);
1154 gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
1155 hig_workarea_add_row(t, &row, _("Have:"), l, NULL);
1156
1157 /* uploaded */
1158 l = di->ul_lb = gtk_label_new(NULL);
1159 gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
1160 hig_workarea_add_row(t, &row, _("Uploaded:"), l, NULL);
1161
1162 /* downloaded */
1163 l = di->dl_lb = gtk_label_new(NULL);
1164 gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
1165 hig_workarea_add_row(t, &row, _("Downloaded:"), l, NULL);
1166
1167 /* state */
1168 l = di->state_lb = gtk_label_new(NULL);
1169 gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
1170 hig_workarea_add_row(t, &row, _("State:"), l, NULL);
1171
1172 /* running for */
1173 l = di->date_started_lb = gtk_label_new(NULL);
1174 gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
1175 hig_workarea_add_row(t, &row, _("Running time:"), l, NULL);
1176
1177 /* eta */
1178 l = di->eta_lb = gtk_label_new(NULL);
1179 gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
1180 hig_workarea_add_row(t, &row, _("Remaining time:"), l, NULL);
1181
1182 /* last activity */
1183 l = di->last_activity_lb = gtk_label_new(NULL);
1184 gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
1185 hig_workarea_add_row(t, &row, _("Last activity:"), l, NULL);
1186
1187 /* error */
1188 l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
1189 hig_workarea_add_row(t, &row, _("Error:"), l, NULL);
1190 di->error_lb = l;
1191
1192 hig_workarea_add_section_divider(t, &row);
1193 hig_workarea_add_section_title(t, &row, _("Details"));
1194
1195 /* destination */
1196 l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
1197 hig_workarea_add_row(t, &row, _("Location:"), l, NULL);
1198 di->destination_lb = l;
1199
1200 /* hash */
1201 l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
1202 hig_workarea_add_row(t, &row, _("Hash:"), l, NULL);
1203 di->hash_lb = l;
1204
1205 /* privacy */
1206 l = gtk_label_new(NULL);
1207 gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
1208 hig_workarea_add_row(t, &row, _("Privacy:"), l, NULL);
1209 di->privacy_lb = l;
1210
1211 /* origins */
1212 l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
1213 hig_workarea_add_row(t, &row, _("Origin:"), l, NULL);
1214 di->origin_lb = l;
1215
1216 /* comment */
1217 b = di->comment_buffer = gtk_text_buffer_new(NULL);
1218 w = gtk_text_view_new_with_buffer(b);
1219 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(w), GTK_WRAP_WORD);
1220 gtk_text_view_set_editable(GTK_TEXT_VIEW(w), FALSE);
1221 sw = gtk_scrolled_window_new(NULL, NULL);
1222 gtk_widget_set_size_request(sw, 350, 36);
1223 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
1224 gtk_container_add(GTK_CONTAINER(sw), w);
1225 fr = gtk_frame_new(NULL);
1226 gtk_frame_set_shadow_type(GTK_FRAME(fr), GTK_SHADOW_IN);
1227 gtk_container_add(GTK_CONTAINER(fr), sw);
1228 w = hig_workarea_add_tall_row(t, &row, _("Comment:"), fr, NULL);
1229 g_object_set(w, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_START, NULL);
1230
1231 hig_workarea_add_section_divider(t, &row);
1232 return t;
1233 }
1234
1235 /****
1236 *****
1237 ***** PEERS TAB
1238 *****
1239 ****/
1240
1241 enum
1242 {
1243 WEBSEED_COL_KEY,
1244 WEBSEED_COL_WAS_UPDATED,
1245 WEBSEED_COL_URL,
1246 WEBSEED_COL_DOWNLOAD_RATE_DOUBLE,
1247 WEBSEED_COL_DOWNLOAD_RATE_STRING,
1248 N_WEBSEED_COLS
1249 };
1250
getWebseedColumnNames(int column)1251 static char const* getWebseedColumnNames(int column)
1252 {
1253 switch (column)
1254 {
1255 case WEBSEED_COL_URL:
1256 return _("Web Seeds");
1257
1258 case WEBSEED_COL_DOWNLOAD_RATE_DOUBLE:
1259 case WEBSEED_COL_DOWNLOAD_RATE_STRING:
1260 return _("Down");
1261
1262 default:
1263 return "";
1264 }
1265 }
1266
webseed_model_new(void)1267 static GtkListStore* webseed_model_new(void)
1268 {
1269 return gtk_list_store_new(N_WEBSEED_COLS,
1270 G_TYPE_STRING, /* key */
1271 G_TYPE_BOOLEAN, /* was-updated */
1272 G_TYPE_STRING, /* url */
1273 G_TYPE_DOUBLE, /* download rate double */
1274 G_TYPE_STRING); /* download rate string */
1275 }
1276
1277 enum
1278 {
1279 PEER_COL_KEY,
1280 PEER_COL_WAS_UPDATED,
1281 PEER_COL_ADDRESS,
1282 PEER_COL_ADDRESS_COLLATED,
1283 PEER_COL_DOWNLOAD_RATE_DOUBLE,
1284 PEER_COL_DOWNLOAD_RATE_STRING,
1285 PEER_COL_UPLOAD_RATE_DOUBLE,
1286 PEER_COL_UPLOAD_RATE_STRING,
1287 PEER_COL_CLIENT,
1288 PEER_COL_PROGRESS,
1289 PEER_COL_UPLOAD_REQUEST_COUNT_INT,
1290 PEER_COL_UPLOAD_REQUEST_COUNT_STRING,
1291 PEER_COL_DOWNLOAD_REQUEST_COUNT_INT,
1292 PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING,
1293 PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT,
1294 PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING,
1295 PEER_COL_BLOCKS_UPLOADED_COUNT_INT,
1296 PEER_COL_BLOCKS_UPLOADED_COUNT_STRING,
1297 PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT,
1298 PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING,
1299 PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT,
1300 PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING,
1301 PEER_COL_ENCRYPTION_STOCK_ID,
1302 PEER_COL_FLAGS,
1303 PEER_COL_TORRENT_NAME,
1304 N_PEER_COLS
1305 };
1306
getPeerColumnName(int column)1307 static char const* getPeerColumnName(int column)
1308 {
1309 switch (column)
1310 {
1311 case PEER_COL_ADDRESS:
1312 return _("Address");
1313
1314 case PEER_COL_DOWNLOAD_RATE_STRING:
1315 case PEER_COL_DOWNLOAD_RATE_DOUBLE:
1316 return _("Down");
1317
1318 case PEER_COL_UPLOAD_RATE_STRING:
1319 case PEER_COL_UPLOAD_RATE_DOUBLE:
1320 return _("Up");
1321
1322 case PEER_COL_CLIENT:
1323 return _("Client");
1324
1325 case PEER_COL_PROGRESS:
1326 return _("%");
1327
1328 case PEER_COL_UPLOAD_REQUEST_COUNT_INT:
1329 case PEER_COL_UPLOAD_REQUEST_COUNT_STRING:
1330 return _("Up Reqs");
1331
1332 case PEER_COL_DOWNLOAD_REQUEST_COUNT_INT:
1333 case PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING:
1334 return _("Dn Reqs");
1335
1336 case PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT:
1337 case PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING:
1338 return _("Dn Blocks");
1339
1340 case PEER_COL_BLOCKS_UPLOADED_COUNT_INT:
1341 case PEER_COL_BLOCKS_UPLOADED_COUNT_STRING:
1342 return _("Up Blocks");
1343
1344 case PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT:
1345 case PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING:
1346 return _("We Cancelled");
1347
1348 case PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT:
1349 case PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING:
1350 return _("They Cancelled");
1351
1352 case PEER_COL_FLAGS:
1353 return _("Flags");
1354
1355 default:
1356 return "";
1357 }
1358 }
1359
peer_store_new(void)1360 static GtkListStore* peer_store_new(void)
1361 {
1362 return gtk_list_store_new(N_PEER_COLS,
1363 G_TYPE_STRING, /* key */
1364 G_TYPE_BOOLEAN, /* was-updated */
1365 G_TYPE_STRING, /* address */
1366 G_TYPE_STRING, /* collated address */
1367 G_TYPE_DOUBLE, /* download speed int */
1368 G_TYPE_STRING, /* download speed string */
1369 G_TYPE_DOUBLE, /* upload speed int */
1370 G_TYPE_STRING, /* upload speed string */
1371 G_TYPE_STRING, /* client */
1372 G_TYPE_INT, /* progress [0..100] */
1373 G_TYPE_INT, /* upload request count int */
1374 G_TYPE_STRING, /* upload request count string */
1375 G_TYPE_INT, /* download request count int */
1376 G_TYPE_STRING, /* download request count string */
1377 G_TYPE_INT, /* # blocks downloaded int */
1378 G_TYPE_STRING, /* # blocks downloaded string */
1379 G_TYPE_INT, /* # blocks uploaded int */
1380 G_TYPE_STRING, /* # blocks uploaded string */
1381 G_TYPE_INT, /* # blocks cancelled by client int */
1382 G_TYPE_STRING, /* # blocks cancelled by client string */
1383 G_TYPE_INT, /* # blocks cancelled by peer int */
1384 G_TYPE_STRING, /* # blocks cancelled by peer string */
1385 G_TYPE_STRING, /* encryption stock id */
1386 G_TYPE_STRING, /* flagString */
1387 G_TYPE_STRING); /* torrent name */
1388 }
1389
initPeerRow(GtkListStore * store,GtkTreeIter * iter,char const * key,char const * torrentName,tr_peer_stat const * peer)1390 static void initPeerRow(GtkListStore* store, GtkTreeIter* iter, char const* key, char const* torrentName,
1391 tr_peer_stat const* peer)
1392 {
1393 int q[4];
1394 char collated_name[128];
1395 char const* client = peer->client;
1396
1397 if (client == NULL || g_strcmp0(client, "Unknown Client") == 0)
1398 {
1399 client = "";
1400 }
1401
1402 if (sscanf(peer->addr, "%d.%d.%d.%d", q, q + 1, q + 2, q + 3) != 4)
1403 {
1404 g_strlcpy(collated_name, peer->addr, sizeof(collated_name));
1405 }
1406 else
1407 {
1408 g_snprintf(collated_name, sizeof(collated_name), "%03d.%03d.%03d.%03d", q[0], q[1], q[2], q[3]);
1409 }
1410
1411 gtk_list_store_set(store, iter,
1412 PEER_COL_ADDRESS, peer->addr,
1413 PEER_COL_ADDRESS_COLLATED, collated_name,
1414 PEER_COL_CLIENT, client,
1415 PEER_COL_ENCRYPTION_STOCK_ID, peer->isEncrypted ? "transmission-lock" : NULL,
1416 PEER_COL_KEY, key,
1417 PEER_COL_TORRENT_NAME, torrentName,
1418 -1);
1419 }
1420
refreshPeerRow(GtkListStore * store,GtkTreeIter * iter,tr_peer_stat const * peer)1421 static void refreshPeerRow(GtkListStore* store, GtkTreeIter* iter, tr_peer_stat const* peer)
1422 {
1423 char up_speed[64] = { '\0' };
1424 char down_speed[64] = { '\0' };
1425 char up_count[64] = { '\0' };
1426 char down_count[64] = { '\0' };
1427 char blocks_to_peer[64] = { '\0' };
1428 char blocks_to_client[64] = { '\0' };
1429 char cancelled_by_peer[64] = { '\0' };
1430 char cancelled_by_client[64] = { '\0' };
1431
1432 if (peer->rateToPeer_KBps > 0.01)
1433 {
1434 tr_formatter_speed_KBps(up_speed, peer->rateToPeer_KBps, sizeof(up_speed));
1435 }
1436
1437 if (peer->rateToClient_KBps > 0)
1438 {
1439 tr_formatter_speed_KBps(down_speed, peer->rateToClient_KBps, sizeof(down_speed));
1440 }
1441
1442 if (peer->pendingReqsToPeer > 0)
1443 {
1444 g_snprintf(down_count, sizeof(down_count), "%d", peer->pendingReqsToPeer);
1445 }
1446
1447 if (peer->pendingReqsToClient > 0)
1448 {
1449 g_snprintf(up_count, sizeof(down_count), "%d", peer->pendingReqsToClient);
1450 }
1451
1452 if (peer->blocksToPeer > 0)
1453 {
1454 g_snprintf(blocks_to_peer, sizeof(blocks_to_peer), "%" PRIu32, peer->blocksToPeer);
1455 }
1456
1457 if (peer->blocksToClient > 0)
1458 {
1459 g_snprintf(blocks_to_client, sizeof(blocks_to_client), "%" PRIu32, peer->blocksToClient);
1460 }
1461
1462 if (peer->cancelsToPeer > 0)
1463 {
1464 g_snprintf(cancelled_by_client, sizeof(cancelled_by_client), "%" PRIu32, peer->cancelsToPeer);
1465 }
1466
1467 if (peer->cancelsToClient > 0)
1468 {
1469 g_snprintf(cancelled_by_peer, sizeof(cancelled_by_peer), "%" PRIu32, peer->cancelsToClient);
1470 }
1471
1472 gtk_list_store_set(store, iter,
1473 PEER_COL_PROGRESS, (int)(100.0 * peer->progress),
1474 PEER_COL_UPLOAD_REQUEST_COUNT_INT, peer->pendingReqsToClient,
1475 PEER_COL_UPLOAD_REQUEST_COUNT_STRING, up_count,
1476 PEER_COL_DOWNLOAD_REQUEST_COUNT_INT, peer->pendingReqsToPeer,
1477 PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING, down_count,
1478 PEER_COL_DOWNLOAD_RATE_DOUBLE, peer->rateToClient_KBps,
1479 PEER_COL_DOWNLOAD_RATE_STRING, down_speed,
1480 PEER_COL_UPLOAD_RATE_DOUBLE, peer->rateToPeer_KBps,
1481 PEER_COL_UPLOAD_RATE_STRING, up_speed,
1482 PEER_COL_FLAGS, peer->flagStr,
1483 PEER_COL_WAS_UPDATED, TRUE,
1484 PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT, (int)peer->blocksToClient,
1485 PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING, blocks_to_client,
1486 PEER_COL_BLOCKS_UPLOADED_COUNT_INT, (int)peer->blocksToPeer,
1487 PEER_COL_BLOCKS_UPLOADED_COUNT_STRING, blocks_to_peer,
1488 PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT, (int)peer->cancelsToPeer,
1489 PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING, cancelled_by_client,
1490 PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT, (int)peer->cancelsToClient,
1491 PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING, cancelled_by_peer,
1492 -1);
1493 }
1494
refreshPeerList(struct DetailsImpl * di,tr_torrent ** torrents,int n)1495 static void refreshPeerList(struct DetailsImpl* di, tr_torrent** torrents, int n)
1496 {
1497 int* peerCount;
1498 GtkTreeIter iter;
1499 GtkTreeModel* model;
1500 GHashTable* hash = di->peer_hash;
1501 GtkListStore* store = di->peer_store;
1502 struct tr_peer_stat** peers;
1503
1504 /* step 1: get all the peers */
1505 peers = g_new(struct tr_peer_stat*, n);
1506 peerCount = g_new(int, n);
1507
1508 for (int i = 0; i < n; ++i)
1509 {
1510 peers[i] = tr_torrentPeers(torrents[i], &peerCount[i]);
1511 }
1512
1513 /* step 2: mark all the peers in the list as not-updated */
1514 model = GTK_TREE_MODEL(store);
1515
1516 if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
1517 {
1518 do
1519 {
1520 gtk_list_store_set(store, &iter, PEER_COL_WAS_UPDATED, FALSE, -1);
1521 }
1522 while (gtk_tree_model_iter_next(model, &iter));
1523 }
1524
1525 /* step 3: add any new peers */
1526 for (int i = 0; i < n; ++i)
1527 {
1528 tr_torrent const* tor = torrents[i];
1529
1530 for (int j = 0; j < peerCount[i]; ++j)
1531 {
1532 char key[128];
1533 tr_peer_stat const* s = &peers[i][j];
1534
1535 g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), s->addr);
1536
1537 if (g_hash_table_lookup(hash, key) == NULL)
1538 {
1539 GtkTreePath* p;
1540 gtk_list_store_append(store, &iter);
1541 initPeerRow(store, &iter, key, tr_torrentName(tor), s);
1542 p = gtk_tree_model_get_path(model, &iter);
1543 g_hash_table_insert(hash, g_strdup(key), gtk_tree_row_reference_new(model, p));
1544 gtk_tree_path_free(p);
1545 }
1546 }
1547 }
1548
1549 /* step 4: update the peers */
1550 for (int i = 0; i < n; ++i)
1551 {
1552 tr_torrent const* tor = torrents[i];
1553
1554 for (int j = 0; j < peerCount[i]; ++j)
1555 {
1556 char key[128];
1557 GtkTreePath* p;
1558 GtkTreeRowReference* ref;
1559 tr_peer_stat const* s = &peers[i][j];
1560
1561 g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), s->addr);
1562 ref = g_hash_table_lookup(hash, key);
1563 p = gtk_tree_row_reference_get_path(ref);
1564 gtk_tree_model_get_iter(model, &iter, p);
1565 refreshPeerRow(store, &iter, s);
1566 gtk_tree_path_free(p);
1567 }
1568 }
1569
1570 /* step 5: remove peers that have disappeared */
1571 model = GTK_TREE_MODEL(store);
1572
1573 if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
1574 {
1575 gboolean more = TRUE;
1576
1577 while (more)
1578 {
1579 gboolean b;
1580 gtk_tree_model_get(model, &iter, PEER_COL_WAS_UPDATED, &b, -1);
1581
1582 if (b)
1583 {
1584 more = gtk_tree_model_iter_next(model, &iter);
1585 }
1586 else
1587 {
1588 char* key;
1589 gtk_tree_model_get(model, &iter, PEER_COL_KEY, &key, -1);
1590 g_hash_table_remove(hash, key);
1591 more = gtk_list_store_remove(store, &iter);
1592 g_free(key);
1593 }
1594 }
1595 }
1596
1597 /* step 6: cleanup */
1598 for (int i = 0; i < n; ++i)
1599 {
1600 tr_torrentPeersFree(peers[i], peerCount[i]);
1601 }
1602
1603 tr_free(peers);
1604 tr_free(peerCount);
1605 }
1606
refreshWebseedList(struct DetailsImpl * di,tr_torrent ** torrents,int n)1607 static void refreshWebseedList(struct DetailsImpl* di, tr_torrent** torrents, int n)
1608 {
1609 int total = 0;
1610 GtkTreeIter iter;
1611 GHashTable* hash = di->webseed_hash;
1612 GtkListStore* store = di->webseed_store;
1613 GtkTreeModel* model = GTK_TREE_MODEL(store);
1614
1615 /* step 1: mark all webseeds as not-updated */
1616 if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
1617 {
1618 do
1619 {
1620 gtk_list_store_set(store, &iter, WEBSEED_COL_WAS_UPDATED, FALSE, -1);
1621 }
1622 while (gtk_tree_model_iter_next(model, &iter));
1623 }
1624
1625 /* step 2: add any new webseeds */
1626 for (int i = 0; i < n; ++i)
1627 {
1628 tr_torrent const* tor = torrents[i];
1629 tr_info const* inf = tr_torrentInfo(tor);
1630
1631 total += inf->webseedCount;
1632
1633 for (unsigned int j = 0; j < inf->webseedCount; ++j)
1634 {
1635 char key[256];
1636 char const* url = inf->webseeds[j];
1637 g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), url);
1638
1639 if (g_hash_table_lookup(hash, key) == NULL)
1640 {
1641 GtkTreePath* p;
1642 gtk_list_store_append(store, &iter);
1643 gtk_list_store_set(store, &iter,
1644 WEBSEED_COL_URL, url,
1645 WEBSEED_COL_KEY, key,
1646 -1);
1647 p = gtk_tree_model_get_path(model, &iter);
1648 g_hash_table_insert(hash, g_strdup(key), gtk_tree_row_reference_new(model, p));
1649 gtk_tree_path_free(p);
1650 }
1651 }
1652 }
1653
1654 /* step 3: update the webseeds */
1655 for (int i = 0; i < n; ++i)
1656 {
1657 tr_torrent* tor = torrents[i];
1658 tr_info const* inf = tr_torrentInfo(tor);
1659 double* speeds_KBps = tr_torrentWebSpeeds_KBps(tor);
1660
1661 for (unsigned int j = 0; j < inf->webseedCount; ++j)
1662 {
1663 char buf[128];
1664 char key[256];
1665 GtkTreePath* p;
1666 GtkTreeRowReference* ref;
1667 char const* url = inf->webseeds[j];
1668
1669 g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), url);
1670 ref = g_hash_table_lookup(hash, key);
1671 p = gtk_tree_row_reference_get_path(ref);
1672 gtk_tree_model_get_iter(model, &iter, p);
1673
1674 if (speeds_KBps[j] > 0)
1675 {
1676 tr_formatter_speed_KBps(buf, speeds_KBps[j], sizeof(buf));
1677 }
1678 else
1679 {
1680 *buf = '\0';
1681 }
1682
1683 gtk_list_store_set(store, &iter,
1684 WEBSEED_COL_DOWNLOAD_RATE_DOUBLE, speeds_KBps[j],
1685 WEBSEED_COL_DOWNLOAD_RATE_STRING, buf,
1686 WEBSEED_COL_WAS_UPDATED, TRUE,
1687 -1);
1688
1689 gtk_tree_path_free(p);
1690 }
1691
1692 tr_free(speeds_KBps);
1693 }
1694
1695 /* step 4: remove webseeds that have disappeared */
1696 if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
1697 {
1698 gboolean more = TRUE;
1699
1700 while (more)
1701 {
1702 gboolean b;
1703 gtk_tree_model_get(model, &iter, WEBSEED_COL_WAS_UPDATED, &b, -1);
1704
1705 if (b)
1706 {
1707 more = gtk_tree_model_iter_next(model, &iter);
1708 }
1709 else
1710 {
1711 char* key;
1712 gtk_tree_model_get(model, &iter, WEBSEED_COL_KEY, &key, -1);
1713
1714 if (key != NULL)
1715 {
1716 g_hash_table_remove(hash, key);
1717 }
1718
1719 more = gtk_list_store_remove(store, &iter);
1720 g_free(key);
1721 }
1722 }
1723 }
1724
1725 /* most of the time there are no webseeds...
1726 don't waste space showing an empty list */
1727 gtk_widget_set_visible(di->webseed_view, total > 0);
1728 }
1729
refreshPeers(struct DetailsImpl * di,tr_torrent ** torrents,int n)1730 static void refreshPeers(struct DetailsImpl* di, tr_torrent** torrents, int n)
1731 {
1732 refreshPeerList(di, torrents, n);
1733 refreshWebseedList(di, torrents, n);
1734 }
1735
onPeerViewQueryTooltip(GtkWidget * widget,gint x,gint y,gboolean keyboard_tip,GtkTooltip * tooltip,gpointer gdi)1736 static gboolean onPeerViewQueryTooltip(GtkWidget* widget, gint x, gint y, gboolean keyboard_tip, GtkTooltip* tooltip,
1737 gpointer gdi)
1738 {
1739 GtkTreeIter iter;
1740 GtkTreeModel* model;
1741 gboolean show_tip = FALSE;
1742
1743 if (gtk_tree_view_get_tooltip_context(GTK_TREE_VIEW(widget), &x, &y, keyboard_tip, &model, NULL, &iter))
1744 {
1745 char* name = NULL;
1746 char* addr = NULL;
1747 char* markup = NULL;
1748 char* flagstr = NULL;
1749 struct DetailsImpl* di = gdi;
1750 GString* gstr = di->gstr;
1751
1752 gtk_tree_model_get(model, &iter,
1753 PEER_COL_TORRENT_NAME, &name,
1754 PEER_COL_ADDRESS, &addr,
1755 PEER_COL_FLAGS, &flagstr,
1756 -1);
1757
1758 g_string_truncate(gstr, 0);
1759 markup = g_markup_escape_text(name, -1);
1760 g_string_append_printf(gstr, "<b>%s</b>\n%s\n \n", markup, addr);
1761 g_free(markup);
1762
1763 for (char const* pch = flagstr; !tr_str_is_empty(pch); ++pch)
1764 {
1765 char const* s = NULL;
1766
1767 switch (*pch)
1768 {
1769 case 'O':
1770 s = _("Optimistic unchoke");
1771 break;
1772
1773 case 'D':
1774 s = _("Downloading from this peer");
1775 break;
1776
1777 case 'd':
1778 s = _("We would download from this peer if they would let us");
1779 break;
1780
1781 case 'U':
1782 s = _("Uploading to peer");
1783 break;
1784
1785 case 'u':
1786 s = _("We would upload to this peer if they asked");
1787 break;
1788
1789 case 'K':
1790 s = _("Peer has unchoked us, but we're not interested");
1791 break;
1792
1793 case '?':
1794 s = _("We unchoked this peer, but they're not interested");
1795 break;
1796
1797 case 'E':
1798 s = _("Encrypted connection");
1799 break;
1800
1801 case 'X':
1802 s = _("Peer was found through Peer Exchange (PEX)");
1803 break;
1804
1805 case 'H':
1806 s = _("Peer was found through DHT");
1807 break;
1808
1809 case 'I':
1810 s = _("Peer is an incoming connection");
1811 break;
1812
1813 case 'T':
1814 s = _("Peer is connected over µTP");
1815 break;
1816 }
1817
1818 if (s != NULL)
1819 {
1820 g_string_append_printf(gstr, "%c: %s\n", *pch, s);
1821 }
1822 }
1823
1824 if (gstr->len != 0) /* remove the last linefeed */
1825 {
1826 g_string_set_size(gstr, gstr->len - 1);
1827 }
1828
1829 gtk_tooltip_set_markup(tooltip, gstr->str);
1830
1831 g_free(flagstr);
1832 g_free(addr);
1833 g_free(name);
1834 show_tip = TRUE;
1835 }
1836
1837 return show_tip;
1838 }
1839
setPeerViewColumns(GtkTreeView * peer_view)1840 static void setPeerViewColumns(GtkTreeView* peer_view)
1841 {
1842 int n;
1843 int view_columns[32];
1844 GtkCellRenderer* r;
1845 GtkTreeViewColumn* c;
1846 bool const more = gtr_pref_flag_get(TR_KEY_show_extra_peer_details);
1847
1848 n = 0;
1849 view_columns[n++] = PEER_COL_ENCRYPTION_STOCK_ID;
1850 view_columns[n++] = PEER_COL_UPLOAD_RATE_STRING;
1851
1852 if (more)
1853 {
1854 view_columns[n++] = PEER_COL_UPLOAD_REQUEST_COUNT_STRING;
1855 }
1856
1857 view_columns[n++] = PEER_COL_DOWNLOAD_RATE_STRING;
1858
1859 if (more)
1860 {
1861 view_columns[n++] = PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING;
1862 }
1863
1864 if (more)
1865 {
1866 view_columns[n++] = PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING;
1867 }
1868
1869 if (more)
1870 {
1871 view_columns[n++] = PEER_COL_BLOCKS_UPLOADED_COUNT_STRING;
1872 }
1873
1874 if (more)
1875 {
1876 view_columns[n++] = PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING;
1877 }
1878
1879 if (more)
1880 {
1881 view_columns[n++] = PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING;
1882 }
1883
1884 view_columns[n++] = PEER_COL_PROGRESS;
1885 view_columns[n++] = PEER_COL_FLAGS;
1886 view_columns[n++] = PEER_COL_ADDRESS;
1887 view_columns[n++] = PEER_COL_CLIENT;
1888
1889 /* remove any existing columns */
1890 while ((c = gtk_tree_view_get_column(peer_view, 0)) != NULL)
1891 {
1892 gtk_tree_view_remove_column(peer_view, c);
1893 }
1894
1895 for (int i = 0; i < n; ++i)
1896 {
1897 int const col = view_columns[i];
1898 char const* t = getPeerColumnName(col);
1899 int sort_col = col;
1900
1901 switch (col)
1902 {
1903 case PEER_COL_ADDRESS:
1904 r = gtk_cell_renderer_text_new();
1905 c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
1906 sort_col = PEER_COL_ADDRESS_COLLATED;
1907 break;
1908
1909 case PEER_COL_CLIENT:
1910 r = gtk_cell_renderer_text_new();
1911 c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
1912 break;
1913
1914 case PEER_COL_PROGRESS:
1915 r = gtk_cell_renderer_progress_new();
1916 c = gtk_tree_view_column_new_with_attributes(t, r, "value", PEER_COL_PROGRESS, NULL);
1917 break;
1918
1919 case PEER_COL_ENCRYPTION_STOCK_ID:
1920 r = gtk_cell_renderer_pixbuf_new();
1921 g_object_set(r, "xalign", (gfloat)0.0, "yalign", (gfloat)0.5, NULL);
1922 c = gtk_tree_view_column_new_with_attributes(t, r, "stock-id", PEER_COL_ENCRYPTION_STOCK_ID, NULL);
1923 gtk_tree_view_column_set_sizing(c, GTK_TREE_VIEW_COLUMN_FIXED);
1924 gtk_tree_view_column_set_fixed_width(c, 20);
1925 break;
1926
1927 case PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING:
1928 r = gtk_cell_renderer_text_new();
1929 c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
1930 sort_col = PEER_COL_DOWNLOAD_REQUEST_COUNT_INT;
1931 break;
1932
1933 case PEER_COL_UPLOAD_REQUEST_COUNT_STRING:
1934 r = gtk_cell_renderer_text_new();
1935 c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
1936 sort_col = PEER_COL_UPLOAD_REQUEST_COUNT_INT;
1937 break;
1938
1939 case PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING:
1940 r = gtk_cell_renderer_text_new();
1941 c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
1942 sort_col = PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT;
1943 break;
1944
1945 case PEER_COL_BLOCKS_UPLOADED_COUNT_STRING:
1946 r = gtk_cell_renderer_text_new();
1947 c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
1948 sort_col = PEER_COL_BLOCKS_UPLOADED_COUNT_INT;
1949 break;
1950
1951 case PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING:
1952 r = gtk_cell_renderer_text_new();
1953 c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
1954 sort_col = PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT;
1955 break;
1956
1957 case PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING:
1958 r = gtk_cell_renderer_text_new();
1959 c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
1960 sort_col = PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT;
1961 break;
1962
1963 case PEER_COL_DOWNLOAD_RATE_STRING:
1964 r = gtk_cell_renderer_text_new();
1965 g_object_set(G_OBJECT(r), "xalign", 1.0F, NULL);
1966 c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
1967 sort_col = PEER_COL_DOWNLOAD_RATE_DOUBLE;
1968 break;
1969
1970 case PEER_COL_UPLOAD_RATE_STRING:
1971 r = gtk_cell_renderer_text_new();
1972 g_object_set(G_OBJECT(r), "xalign", 1.0F, NULL);
1973 c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
1974 sort_col = PEER_COL_UPLOAD_RATE_DOUBLE;
1975 break;
1976
1977 case PEER_COL_FLAGS:
1978 r = gtk_cell_renderer_text_new();
1979 c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
1980 break;
1981
1982 default:
1983 abort();
1984 }
1985
1986 gtk_tree_view_column_set_resizable(c, FALSE);
1987 gtk_tree_view_column_set_sort_column_id(c, sort_col);
1988 gtk_tree_view_append_column(GTK_TREE_VIEW(peer_view), c);
1989 }
1990
1991 /* the 'expander' column has a 10-pixel margin on the left
1992 that doesn't look quite correct in any of these columns...
1993 so create a non-visible column and assign it as the
1994 'expander column. */
1995 {
1996 GtkTreeViewColumn* c = gtk_tree_view_column_new();
1997 gtk_tree_view_column_set_visible(c, FALSE);
1998 gtk_tree_view_append_column(GTK_TREE_VIEW(peer_view), c);
1999 gtk_tree_view_set_expander_column(GTK_TREE_VIEW(peer_view), c);
2000 }
2001 }
2002
onMorePeerInfoToggled(GtkToggleButton * button,struct DetailsImpl * di)2003 static void onMorePeerInfoToggled(GtkToggleButton* button, struct DetailsImpl* di)
2004 {
2005 tr_quark const key = TR_KEY_show_extra_peer_details;
2006 gboolean const value = gtk_toggle_button_get_active(button);
2007 gtr_core_set_pref_bool(di->core, key, value);
2008 setPeerViewColumns(GTK_TREE_VIEW(di->peer_view));
2009 }
2010
peer_page_new(struct DetailsImpl * di)2011 static GtkWidget* peer_page_new(struct DetailsImpl* di)
2012 {
2013 gboolean b;
2014 char const* str;
2015 GtkListStore* store;
2016 GtkWidget* v;
2017 GtkWidget* w;
2018 GtkWidget* ret;
2019 GtkWidget* sw;
2020 GtkWidget* vbox;
2021 GtkWidget* webtree = NULL;
2022 GtkTreeModel* m;
2023 GtkTreeViewColumn* c;
2024 GtkCellRenderer* r;
2025
2026 /* webseeds */
2027
2028 store = di->webseed_store = webseed_model_new();
2029 v = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
2030 g_signal_connect(v, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL);
2031 g_object_unref(store);
2032
2033 str = getWebseedColumnNames(WEBSEED_COL_URL);
2034 r = gtk_cell_renderer_text_new();
2035 g_object_set(G_OBJECT(r), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
2036 c = gtk_tree_view_column_new_with_attributes(str, r, "text", WEBSEED_COL_URL, NULL);
2037 g_object_set(G_OBJECT(c), "expand", TRUE, NULL);
2038 gtk_tree_view_column_set_sort_column_id(c, WEBSEED_COL_URL);
2039 gtk_tree_view_append_column(GTK_TREE_VIEW(v), c);
2040
2041 str = getWebseedColumnNames(WEBSEED_COL_DOWNLOAD_RATE_STRING);
2042 r = gtk_cell_renderer_text_new();
2043 c = gtk_tree_view_column_new_with_attributes(str, r, "text", WEBSEED_COL_DOWNLOAD_RATE_STRING, NULL);
2044 gtk_tree_view_column_set_sort_column_id(c, WEBSEED_COL_DOWNLOAD_RATE_DOUBLE);
2045 gtk_tree_view_append_column(GTK_TREE_VIEW(v), c);
2046
2047 w = gtk_scrolled_window_new(NULL, NULL);
2048 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
2049 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(w), GTK_SHADOW_IN);
2050 gtk_container_add(GTK_CONTAINER(w), v);
2051
2052 webtree = w;
2053 di->webseed_view = w;
2054
2055 /* peers */
2056
2057 store = di->peer_store = peer_store_new();
2058 m = gtk_tree_model_sort_new_with_model(GTK_TREE_MODEL(store));
2059 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(m), PEER_COL_PROGRESS, GTK_SORT_DESCENDING);
2060 v = GTK_WIDGET(g_object_new(GTK_TYPE_TREE_VIEW, "model", m, "rules-hint", TRUE, "has-tooltip", TRUE, NULL));
2061 di->peer_view = v;
2062
2063 g_signal_connect(v, "query-tooltip", G_CALLBACK(onPeerViewQueryTooltip), di);
2064 g_object_unref(store);
2065 g_signal_connect(v, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL);
2066
2067 setPeerViewColumns(GTK_TREE_VIEW(v));
2068
2069 w = sw = gtk_scrolled_window_new(NULL, NULL);
2070 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
2071 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(w), GTK_SHADOW_IN);
2072 gtk_container_add(GTK_CONTAINER(w), v);
2073
2074 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, GUI_PAD);
2075 gtk_container_set_border_width(GTK_CONTAINER(vbox), GUI_PAD_BIG);
2076
2077 v = gtk_paned_new(GTK_ORIENTATION_VERTICAL);
2078 gtk_paned_pack1(GTK_PANED(v), webtree, FALSE, TRUE);
2079 gtk_paned_pack2(GTK_PANED(v), sw, TRUE, TRUE);
2080 gtk_box_pack_start(GTK_BOX(vbox), v, TRUE, TRUE, 0);
2081
2082 w = gtk_check_button_new_with_mnemonic(_("Show _more details"));
2083 di->more_peer_details_check = w;
2084 b = gtr_pref_flag_get(TR_KEY_show_extra_peer_details);
2085 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), b);
2086 g_signal_connect(w, "toggled", G_CALLBACK(onMorePeerInfoToggled), di);
2087 gtk_box_pack_start(GTK_BOX(vbox), w, FALSE, FALSE, 0);
2088
2089 /* ip-to-GtkTreeRowReference */
2090 di->peer_hash = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free,
2091 (GDestroyNotify)gtk_tree_row_reference_free);
2092
2093 /* url-to-GtkTreeRowReference */
2094 di->webseed_hash = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free,
2095 (GDestroyNotify)gtk_tree_row_reference_free);
2096 ret = vbox;
2097 return ret;
2098 }
2099
2100 /****
2101 *****
2102 ***** TRACKER
2103 *****
2104 ****/
2105
2106 /* if it's been longer than a minute, don't bother showing the seconds */
tr_strltime_rounded(char * buf,time_t t,size_t buflen)2107 static void tr_strltime_rounded(char* buf, time_t t, size_t buflen)
2108 {
2109 if (t > 60)
2110 {
2111 t -= (t % 60);
2112 }
2113
2114 tr_strltime(buf, t, buflen);
2115 }
2116
buildTrackerSummary(GString * gstr,char const * key,tr_tracker_stat const * st,gboolean showScrape)2117 static void buildTrackerSummary(GString* gstr, char const* key, tr_tracker_stat const* st, gboolean showScrape)
2118 {
2119 char* str;
2120 char timebuf[256];
2121 time_t const now = time(NULL);
2122 char const* err_markup_begin = "<span color=\"red\">";
2123 char const* err_markup_end = "</span>";
2124 char const* timeout_markup_begin = "<span color=\"#224466\">";
2125 char const* timeout_markup_end = "</span>";
2126 char const* success_markup_begin = "<span color=\"#008B00\">";
2127 char const* success_markup_end = "</span>";
2128
2129 /* hostname */
2130 {
2131 g_string_append(gstr, st->isBackup ? "<i>" : "<b>");
2132
2133 if (key != NULL)
2134 {
2135 str = g_markup_printf_escaped("%s - %s", st->host, key);
2136 }
2137 else
2138 {
2139 str = g_markup_printf_escaped("%s", st->host);
2140 }
2141
2142 g_string_append(gstr, str);
2143 g_free(str);
2144 g_string_append(gstr, st->isBackup ? "</i>" : "</b>");
2145 }
2146
2147 if (!st->isBackup)
2148 {
2149 if (st->hasAnnounced && st->announceState != TR_TRACKER_INACTIVE)
2150 {
2151 g_string_append_c(gstr, '\n');
2152 tr_strltime_rounded(timebuf, now - st->lastAnnounceTime, sizeof(timebuf));
2153
2154 if (st->lastAnnounceSucceeded)
2155 {
2156 g_string_append_printf(gstr, _("Got a list of %1$s%2$'d peers%3$s %4$s ago"),
2157 success_markup_begin, st->lastAnnouncePeerCount, success_markup_end, timebuf);
2158 }
2159 else if (st->lastAnnounceTimedOut)
2160 {
2161 g_string_append_printf(gstr, _("Peer list request %1$stimed out%2$s %3$s ago; will retry"),
2162 timeout_markup_begin, timeout_markup_end, timebuf);
2163 }
2164 else
2165 {
2166 g_string_append_printf(gstr, _("Got an error %1$s\"%2$s\"%3$s %4$s ago"), err_markup_begin,
2167 st->lastAnnounceResult, err_markup_end, timebuf);
2168 }
2169 }
2170
2171 switch (st->announceState)
2172 {
2173 case TR_TRACKER_INACTIVE:
2174 g_string_append_c(gstr, '\n');
2175 g_string_append(gstr, _("No updates scheduled"));
2176 break;
2177
2178 case TR_TRACKER_WAITING:
2179 tr_strltime_rounded(timebuf, st->nextAnnounceTime - now, sizeof(timebuf));
2180 g_string_append_c(gstr, '\n');
2181 g_string_append_printf(gstr, _("Asking for more peers in %s"), timebuf);
2182 break;
2183
2184 case TR_TRACKER_QUEUED:
2185 g_string_append_c(gstr, '\n');
2186 g_string_append(gstr, _("Queued to ask for more peers"));
2187 break;
2188
2189 case TR_TRACKER_ACTIVE:
2190 tr_strltime_rounded(timebuf, now - st->lastAnnounceStartTime, sizeof(timebuf));
2191 g_string_append_c(gstr, '\n');
2192 g_string_append_printf(gstr, _("Asking for more peers now… <small>%s</small>"), timebuf);
2193 break;
2194 }
2195
2196 if (showScrape)
2197 {
2198 if (st->hasScraped)
2199 {
2200 g_string_append_c(gstr, '\n');
2201 tr_strltime_rounded(timebuf, now - st->lastScrapeTime, sizeof(timebuf));
2202
2203 if (st->lastScrapeSucceeded)
2204 {
2205 g_string_append_printf(gstr, _("Tracker had %s%'d seeders and %'d leechers%s %s ago"), success_markup_begin,
2206 st->seederCount, st->leecherCount, success_markup_end, timebuf);
2207 }
2208 else
2209 {
2210 g_string_append_printf(gstr, _("Got a scrape error \"%s%s%s\" %s ago"), err_markup_begin,
2211 st->lastScrapeResult, err_markup_end, timebuf);
2212 }
2213 }
2214
2215 switch (st->scrapeState)
2216 {
2217 case TR_TRACKER_INACTIVE:
2218 break;
2219
2220 case TR_TRACKER_WAITING:
2221 g_string_append_c(gstr, '\n');
2222 tr_strltime_rounded(timebuf, st->nextScrapeTime - now, sizeof(timebuf));
2223 g_string_append_printf(gstr, _("Asking for peer counts in %s"), timebuf);
2224 break;
2225
2226 case TR_TRACKER_QUEUED:
2227 g_string_append_c(gstr, '\n');
2228 g_string_append(gstr, _("Queued to ask for peer counts"));
2229 break;
2230
2231 case TR_TRACKER_ACTIVE:
2232 g_string_append_c(gstr, '\n');
2233 tr_strltime_rounded(timebuf, now - st->lastScrapeStartTime, sizeof(timebuf));
2234 g_string_append_printf(gstr, _("Asking for peer counts now… <small>%s</small>"), timebuf);
2235 break;
2236 }
2237 }
2238 }
2239 }
2240
2241 enum
2242 {
2243 TRACKER_COL_TORRENT_ID,
2244 TRACKER_COL_TEXT,
2245 TRACKER_COL_IS_BACKUP,
2246 TRACKER_COL_TRACKER_ID,
2247 TRACKER_COL_FAVICON,
2248 TRACKER_COL_WAS_UPDATED,
2249 TRACKER_COL_KEY,
2250 TRACKER_N_COLS
2251 };
2252
trackerVisibleFunc(GtkTreeModel * model,GtkTreeIter * iter,gpointer data)2253 static gboolean trackerVisibleFunc(GtkTreeModel* model, GtkTreeIter* iter, gpointer data)
2254 {
2255 gboolean isBackup;
2256 struct DetailsImpl* di = data;
2257
2258 /* show all */
2259 if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(di->all_check)))
2260 {
2261 return TRUE;
2262 }
2263
2264 /* don't show the backups... */
2265 gtk_tree_model_get(model, iter, TRACKER_COL_IS_BACKUP, &isBackup, -1);
2266 return !isBackup;
2267 }
2268
tracker_list_get_current_torrent_id(struct DetailsImpl * di)2269 static int tracker_list_get_current_torrent_id(struct DetailsImpl* di)
2270 {
2271 int torrent_id = -1;
2272
2273 /* if there's only one torrent in the dialog, always use it */
2274 if (torrent_id < 0)
2275 {
2276 if (g_slist_length(di->ids) == 1)
2277 {
2278 torrent_id = GPOINTER_TO_INT(di->ids->data);
2279 }
2280 }
2281
2282 /* otherwise, use the selected tracker's torrent */
2283 if (torrent_id < 0)
2284 {
2285 GtkTreeIter iter;
2286 GtkTreeModel* model;
2287 GtkTreeSelection* sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(di->tracker_view));
2288
2289 if (gtk_tree_selection_get_selected(sel, &model, &iter))
2290 {
2291 gtk_tree_model_get(model, &iter, TRACKER_COL_TORRENT_ID, &torrent_id, -1);
2292 }
2293 }
2294
2295 return torrent_id;
2296 }
2297
tracker_list_get_current_torrent(struct DetailsImpl * di)2298 static tr_torrent* tracker_list_get_current_torrent(struct DetailsImpl* di)
2299 {
2300 int const torrent_id = tracker_list_get_current_torrent_id(di);
2301 return gtr_core_find_torrent(di->core, torrent_id);
2302 }
2303
favicon_ready_cb(gpointer pixbuf,gpointer vreference)2304 static void favicon_ready_cb(gpointer pixbuf, gpointer vreference)
2305 {
2306 GtkTreeIter iter;
2307 GtkTreeRowReference* reference = vreference;
2308
2309 if (pixbuf != NULL)
2310 {
2311 GtkTreePath* path = gtk_tree_row_reference_get_path(reference);
2312 GtkTreeModel* model = gtk_tree_row_reference_get_model(reference);
2313
2314 if (gtk_tree_model_get_iter(model, &iter, path))
2315 {
2316 gtk_list_store_set(GTK_LIST_STORE(model), &iter, TRACKER_COL_FAVICON, pixbuf, -1);
2317 }
2318
2319 gtk_tree_path_free(path);
2320 g_object_unref(pixbuf);
2321 }
2322
2323 gtk_tree_row_reference_free(reference);
2324 }
2325
refreshTracker(struct DetailsImpl * di,tr_torrent ** torrents,int n)2326 static void refreshTracker(struct DetailsImpl* di, tr_torrent** torrents, int n)
2327 {
2328 int* statCount;
2329 tr_tracker_stat** stats;
2330 GtkTreeIter iter;
2331 GtkTreeModel* model;
2332 GString* gstr = di->gstr; /* buffer for temporary strings */
2333 GHashTable* hash = di->tracker_hash;
2334 GtkListStore* store = di->tracker_store;
2335 tr_session* session = gtr_core_session(di->core);
2336 gboolean const showScrape = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(di->scrape_check));
2337
2338 /* step 1: get all the trackers */
2339 statCount = g_new0(int, n);
2340 stats = g_new0(tr_tracker_stat*, n);
2341
2342 for (int i = 0; i < n; ++i)
2343 {
2344 stats[i] = tr_torrentTrackers(torrents[i], &statCount[i]);
2345 }
2346
2347 /* step 2: mark all the trackers in the list as not-updated */
2348 model = GTK_TREE_MODEL(store);
2349
2350 if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
2351 {
2352 do
2353 {
2354 gtk_list_store_set(store, &iter, TRACKER_COL_WAS_UPDATED, FALSE, -1);
2355 }
2356 while (gtk_tree_model_iter_next(model, &iter));
2357 }
2358
2359 /* step 3: add any new trackers */
2360 for (int i = 0; i < n; ++i)
2361 {
2362 int const jn = statCount[i];
2363
2364 for (int j = 0; j < jn; ++j)
2365 {
2366 tr_torrent const* tor = torrents[i];
2367 tr_tracker_stat const* st = &stats[i][j];
2368 int const torrent_id = tr_torrentId(tor);
2369
2370 /* build the key to find the row */
2371 g_string_truncate(gstr, 0);
2372 g_string_append_printf(gstr, "%d\t%d\t%s", torrent_id, st->tier, st->announce);
2373
2374 if (g_hash_table_lookup(hash, gstr->str) == NULL)
2375 {
2376 GtkTreePath* p;
2377 GtkTreeIter iter;
2378 GtkTreeRowReference* ref;
2379
2380 gtk_list_store_insert_with_values(store, &iter, -1,
2381 TRACKER_COL_TORRENT_ID, torrent_id,
2382 TRACKER_COL_TRACKER_ID, st->id,
2383 TRACKER_COL_KEY, gstr->str,
2384 -1);
2385
2386 p = gtk_tree_model_get_path(model, &iter);
2387 ref = gtk_tree_row_reference_new(model, p);
2388 g_hash_table_insert(hash, g_strdup(gstr->str), ref);
2389 ref = gtk_tree_row_reference_new(model, p);
2390 gtr_get_favicon_from_url(session, st->announce, favicon_ready_cb, ref);
2391 gtk_tree_path_free(p);
2392 }
2393 }
2394 }
2395
2396 /* step 4: update the peers */
2397 for (int i = 0; i < n; ++i)
2398 {
2399 tr_torrent const* tor = torrents[i];
2400 char const* summary_name = n > 1 ? tr_torrentName(tor) : NULL;
2401
2402 for (int j = 0; j < statCount[i]; ++j)
2403 {
2404 GtkTreePath* p;
2405 GtkTreeRowReference* ref;
2406 tr_tracker_stat const* st = &stats[i][j];
2407
2408 /* build the key to find the row */
2409 g_string_truncate(gstr, 0);
2410 g_string_append_printf(gstr, "%d\t%d\t%s", tr_torrentId(tor), st->tier, st->announce);
2411 ref = g_hash_table_lookup(hash, gstr->str);
2412 p = gtk_tree_row_reference_get_path(ref);
2413 gtk_tree_model_get_iter(model, &iter, p);
2414
2415 /* update the row */
2416 g_string_truncate(gstr, 0);
2417 buildTrackerSummary(gstr, summary_name, st, showScrape);
2418 gtk_list_store_set(store, &iter,
2419 TRACKER_COL_TEXT, gstr->str,
2420 TRACKER_COL_IS_BACKUP, st->isBackup,
2421 TRACKER_COL_TRACKER_ID, st->id,
2422 TRACKER_COL_WAS_UPDATED, TRUE,
2423 -1);
2424
2425 /* cleanup */
2426 gtk_tree_path_free(p);
2427 }
2428 }
2429
2430 /* step 5: remove trackers that have disappeared */
2431 if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
2432 {
2433 gboolean more = TRUE;
2434
2435 while (more)
2436 {
2437 gboolean b;
2438 gtk_tree_model_get(model, &iter, TRACKER_COL_WAS_UPDATED, &b, -1);
2439
2440 if (b)
2441 {
2442 more = gtk_tree_model_iter_next(model, &iter);
2443 }
2444 else
2445 {
2446 char* key;
2447 gtk_tree_model_get(model, &iter, TRACKER_COL_KEY, &key, -1);
2448 g_hash_table_remove(hash, key);
2449 more = gtk_list_store_remove(store, &iter);
2450 g_free(key);
2451 }
2452 }
2453 }
2454
2455 gtk_widget_set_sensitive(di->edit_trackers_button, tracker_list_get_current_torrent_id(di) >= 0);
2456
2457 /* cleanup */
2458 for (int i = 0; i < n; ++i)
2459 {
2460 tr_torrentTrackersFree(stats[i], statCount[i]);
2461 }
2462
2463 g_free(stats);
2464 g_free(statCount);
2465 }
2466
onScrapeToggled(GtkToggleButton * button,struct DetailsImpl * di)2467 static void onScrapeToggled(GtkToggleButton* button, struct DetailsImpl* di)
2468 {
2469 tr_quark const key = TR_KEY_show_tracker_scrapes;
2470 gboolean const value = gtk_toggle_button_get_active(button);
2471 gtr_core_set_pref_bool(di->core, key, value);
2472 refresh(di);
2473 }
2474
onBackupToggled(GtkToggleButton * button,struct DetailsImpl * di)2475 static void onBackupToggled(GtkToggleButton* button, struct DetailsImpl* di)
2476 {
2477 tr_quark const key = TR_KEY_show_backup_trackers;
2478 gboolean const value = gtk_toggle_button_get_active(button);
2479 gtr_core_set_pref_bool(di->core, key, value);
2480 refresh(di);
2481 }
2482
on_edit_trackers_response(GtkDialog * dialog,int response,gpointer data)2483 static void on_edit_trackers_response(GtkDialog* dialog, int response, gpointer data)
2484 {
2485 gboolean do_destroy = TRUE;
2486 struct DetailsImpl* di = data;
2487
2488 if (response == GTK_RESPONSE_ACCEPT)
2489 {
2490 int n;
2491 int tier;
2492 GtkTextIter start;
2493 GtkTextIter end;
2494 int const torrent_id = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(dialog), TORRENT_ID_KEY));
2495 GtkTextBuffer* text_buffer = g_object_get_qdata(G_OBJECT(dialog), TEXT_BUFFER_KEY);
2496 tr_torrent* tor = gtr_core_find_torrent(di->core, torrent_id);
2497
2498 if (tor != NULL)
2499 {
2500 tr_tracker_info* trackers;
2501 char** tracker_strings;
2502 char* tracker_text;
2503
2504 /* build the array of trackers */
2505 gtk_text_buffer_get_bounds(text_buffer, &start, &end);
2506 tracker_text = gtk_text_buffer_get_text(text_buffer, &start, &end, FALSE);
2507 tracker_strings = g_strsplit(tracker_text, "\n", 0);
2508
2509 trackers = g_new0(tr_tracker_info, g_strv_length(tracker_strings));
2510 n = 0;
2511 tier = 0;
2512
2513 for (int i = 0; tracker_strings[i] != NULL; ++i)
2514 {
2515 char* const str = tracker_strings[i];
2516
2517 if (tr_str_is_empty(str))
2518 {
2519 ++tier;
2520 }
2521 else
2522 {
2523 trackers[n].tier = tier;
2524 trackers[n].announce = str;
2525 ++n;
2526 }
2527 }
2528
2529 /* update the torrent */
2530 if (tr_torrentSetAnnounceList(tor, trackers, n))
2531 {
2532 refresh(di);
2533 }
2534 else
2535 {
2536 GtkWidget* w;
2537 char const* text = _("List contains invalid URLs");
2538 w = gtk_message_dialog_new(GTK_WINDOW(dialog), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s",
2539 text);
2540 gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(w), "%s",
2541 _("Please correct the errors and try again."));
2542 gtk_dialog_run(GTK_DIALOG(w));
2543 gtk_widget_destroy(w);
2544 do_destroy = FALSE;
2545 }
2546
2547 /* cleanup */
2548 g_free(trackers);
2549 g_strfreev(tracker_strings);
2550 g_free(tracker_text);
2551 }
2552 }
2553
2554 if (do_destroy)
2555 {
2556 gtk_widget_destroy(GTK_WIDGET(dialog));
2557 }
2558 }
2559
get_editable_tracker_list(GString * gstr,tr_torrent const * tor)2560 static void get_editable_tracker_list(GString* gstr, tr_torrent const* tor)
2561 {
2562 int tier = 0;
2563 tr_info const* inf = tr_torrentInfo(tor);
2564
2565 for (unsigned int i = 0; i < inf->trackerCount; ++i)
2566 {
2567 tr_tracker_info const* t = &inf->trackers[i];
2568
2569 if (tier != t->tier)
2570 {
2571 tier = t->tier;
2572 g_string_append_c(gstr, '\n');
2573 }
2574
2575 g_string_append_printf(gstr, "%s\n", t->announce);
2576 }
2577
2578 if (gstr->len > 0)
2579 {
2580 g_string_truncate(gstr, gstr->len - 1);
2581 }
2582 }
2583
on_edit_trackers(GtkButton * button,gpointer data)2584 static void on_edit_trackers(GtkButton* button, gpointer data)
2585 {
2586 struct DetailsImpl* di = data;
2587 tr_torrent* tor = tracker_list_get_current_torrent(di);
2588
2589 if (tor != NULL)
2590 {
2591 guint row;
2592 GtkWidget* w;
2593 GtkWidget* d;
2594 GtkWidget* fr;
2595 GtkWidget* t;
2596 GtkWidget* l;
2597 GtkWidget* sw;
2598 GtkWindow* win = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));
2599 GString* gstr = di->gstr; /* buffer for temporary strings */
2600 int const torrent_id = tr_torrentId(tor);
2601
2602 g_string_truncate(gstr, 0);
2603 g_string_append_printf(gstr, _("%s - Edit Trackers"), tr_torrentName(tor));
2604 d = gtk_dialog_new_with_buttons(gstr->str, win, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CANCEL,
2605 GTK_RESPONSE_CANCEL, GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL);
2606 g_signal_connect(d, "response", G_CALLBACK(on_edit_trackers_response), data);
2607
2608 row = 0;
2609 t = hig_workarea_create();
2610 hig_workarea_add_section_title(t, &row, _("Tracker Announce URLs"));
2611
2612 l = gtk_label_new(NULL);
2613 gtk_label_set_markup(GTK_LABEL(l), _("To add a backup URL, add it on the line after the primary URL.\n"
2614 "To add another primary URL, add it after a blank line."));
2615 gtk_label_set_justify(GTK_LABEL(l), GTK_JUSTIFY_LEFT);
2616 g_object_set(l, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_CENTER, NULL);
2617 hig_workarea_add_wide_control(t, &row, l);
2618
2619 w = gtk_text_view_new();
2620 g_string_truncate(gstr, 0);
2621 get_editable_tracker_list(gstr, tor);
2622 gtk_text_buffer_set_text(gtk_text_view_get_buffer(GTK_TEXT_VIEW(w)), gstr->str, -1);
2623 fr = gtk_frame_new(NULL);
2624 gtk_frame_set_shadow_type(GTK_FRAME(fr), GTK_SHADOW_IN);
2625 sw = gtk_scrolled_window_new(NULL, NULL);
2626 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
2627 gtk_container_add(GTK_CONTAINER(sw), w);
2628 gtk_container_add(GTK_CONTAINER(fr), sw);
2629 gtk_widget_set_size_request(fr, 500U, 166U);
2630 hig_workarea_add_wide_tall_control(t, &row, fr);
2631
2632 gtr_dialog_set_content(GTK_DIALOG(d), t);
2633
2634 g_object_set_qdata(G_OBJECT(d), TORRENT_ID_KEY, GINT_TO_POINTER(torrent_id));
2635 g_object_set_qdata(G_OBJECT(d), TEXT_BUFFER_KEY, gtk_text_view_get_buffer(GTK_TEXT_VIEW(w)));
2636 gtk_widget_show(d);
2637 }
2638 }
2639
on_tracker_list_selection_changed(GtkTreeSelection * sel,gpointer gdi)2640 static void on_tracker_list_selection_changed(GtkTreeSelection* sel, gpointer gdi)
2641 {
2642 struct DetailsImpl* di = gdi;
2643 int const n = gtk_tree_selection_count_selected_rows(sel);
2644 tr_torrent* tor = tracker_list_get_current_torrent(di);
2645
2646 gtk_widget_set_sensitive(di->remove_tracker_button, n > 0);
2647 gtk_widget_set_sensitive(di->add_tracker_button, tor != NULL);
2648 gtk_widget_set_sensitive(di->edit_trackers_button, tor != NULL);
2649 }
2650
on_add_tracker_response(GtkDialog * dialog,int response,gpointer gdi)2651 static void on_add_tracker_response(GtkDialog* dialog, int response, gpointer gdi)
2652 {
2653 gboolean destroy = TRUE;
2654
2655 if (response == GTK_RESPONSE_ACCEPT)
2656 {
2657 struct DetailsImpl* di = gdi;
2658 GtkWidget* e = GTK_WIDGET(g_object_get_qdata(G_OBJECT(dialog), URL_ENTRY_KEY));
2659 int const torrent_id = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(dialog), TORRENT_ID_KEY));
2660 char* url = g_strdup(gtk_entry_get_text(GTK_ENTRY(e)));
2661 g_strstrip(url);
2662
2663 if (!tr_str_is_empty(url))
2664 {
2665 if (tr_urlIsValidTracker(url))
2666 {
2667 tr_variant top;
2668 tr_variant* args;
2669 tr_variant* trackers;
2670
2671 tr_variantInitDict(&top, 2);
2672 tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set");
2673 args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
2674 tr_variantDictAddInt(args, TR_KEY_id, torrent_id);
2675 trackers = tr_variantDictAddList(args, TR_KEY_trackerAdd, 1);
2676 tr_variantListAddStr(trackers, url);
2677
2678 gtr_core_exec(di->core, &top);
2679 refresh(di);
2680
2681 tr_variantFree(&top);
2682 }
2683 else
2684 {
2685 gtr_unrecognized_url_dialog(GTK_WIDGET(dialog), url);
2686 destroy = FALSE;
2687 }
2688 }
2689
2690 g_free(url);
2691 }
2692
2693 if (destroy)
2694 {
2695 gtk_widget_destroy(GTK_WIDGET(dialog));
2696 }
2697 }
2698
on_tracker_list_add_button_clicked(GtkButton * button UNUSED,gpointer gdi)2699 static void on_tracker_list_add_button_clicked(GtkButton* button UNUSED, gpointer gdi)
2700 {
2701 struct DetailsImpl* di = gdi;
2702 tr_torrent* tor = tracker_list_get_current_torrent(di);
2703
2704 if (tor != NULL)
2705 {
2706 guint row;
2707 GtkWidget* e;
2708 GtkWidget* t;
2709 GtkWidget* w;
2710 GString* gstr = di->gstr; /* buffer for temporary strings */
2711
2712 g_string_truncate(gstr, 0);
2713 g_string_append_printf(gstr, _("%s - Add Tracker"), tr_torrentName(tor));
2714 w = gtk_dialog_new_with_buttons(gstr->str, GTK_WINDOW(di->dialog), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CANCEL,
2715 GTK_RESPONSE_CANCEL, GTK_STOCK_ADD, GTK_RESPONSE_ACCEPT, NULL);
2716 g_signal_connect(w, "response", G_CALLBACK(on_add_tracker_response), gdi);
2717
2718 row = 0;
2719 t = hig_workarea_create();
2720 hig_workarea_add_section_title(t, &row, _("Tracker"));
2721 e = gtk_entry_new();
2722 gtk_widget_set_size_request(e, 400, -1);
2723 gtr_paste_clipboard_url_into_entry(e);
2724 g_object_set_qdata(G_OBJECT(w), URL_ENTRY_KEY, e);
2725 g_object_set_qdata(G_OBJECT(w), TORRENT_ID_KEY, GINT_TO_POINTER(tr_torrentId(tor)));
2726 hig_workarea_add_row(t, &row, _("_Announce URL:"), e, NULL);
2727 gtr_dialog_set_content(GTK_DIALOG(w), t);
2728 gtk_widget_show_all(w);
2729 }
2730 }
2731
on_tracker_list_remove_button_clicked(GtkButton * button UNUSED,gpointer gdi)2732 static void on_tracker_list_remove_button_clicked(GtkButton* button UNUSED, gpointer gdi)
2733 {
2734 GtkTreeIter iter;
2735 GtkTreeModel* model;
2736 struct DetailsImpl* di = gdi;
2737 GtkTreeView* v = GTK_TREE_VIEW(di->tracker_view);
2738 GtkTreeSelection* sel = gtk_tree_view_get_selection(v);
2739
2740 if (gtk_tree_selection_get_selected(sel, &model, &iter))
2741 {
2742 int torrent_id;
2743 int tracker_id;
2744 tr_variant top;
2745 tr_variant* args;
2746 tr_variant* trackers;
2747
2748 gtk_tree_model_get(model, &iter,
2749 TRACKER_COL_TRACKER_ID, &tracker_id,
2750 TRACKER_COL_TORRENT_ID, &torrent_id,
2751 -1);
2752
2753 tr_variantInitDict(&top, 2);
2754 tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set");
2755 args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
2756 tr_variantDictAddInt(args, TR_KEY_id, torrent_id);
2757 trackers = tr_variantDictAddList(args, TR_KEY_trackerRemove, 1);
2758 tr_variantListAddInt(trackers, tracker_id);
2759
2760 gtr_core_exec(di->core, &top);
2761 refresh(di);
2762
2763 tr_variantFree(&top);
2764 }
2765 }
2766
tracker_page_new(struct DetailsImpl * di)2767 static GtkWidget* tracker_page_new(struct DetailsImpl* di)
2768 {
2769 gboolean b;
2770 GtkCellRenderer* r;
2771 GtkTreeViewColumn* c;
2772 GtkTreeSelection* sel;
2773 GtkWidget* vbox;
2774 GtkWidget* sw;
2775 GtkWidget* w;
2776 GtkWidget* v;
2777 GtkWidget* hbox;
2778 int const pad = (GUI_PAD + GUI_PAD_BIG) / 2;
2779
2780 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, GUI_PAD);
2781 gtk_container_set_border_width(GTK_CONTAINER(vbox), GUI_PAD_BIG);
2782
2783 di->tracker_store = gtk_list_store_new(TRACKER_N_COLS,
2784 G_TYPE_INT,
2785 G_TYPE_STRING,
2786 G_TYPE_BOOLEAN,
2787 G_TYPE_INT,
2788 GDK_TYPE_PIXBUF,
2789 G_TYPE_BOOLEAN,
2790 G_TYPE_STRING);
2791
2792 di->tracker_hash = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free,
2793 (GDestroyNotify)gtk_tree_row_reference_free);
2794
2795 di->trackers_filtered = gtk_tree_model_filter_new(GTK_TREE_MODEL(di->tracker_store), NULL);
2796 gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(di->trackers_filtered), trackerVisibleFunc, di, NULL);
2797
2798 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD_BIG);
2799
2800 v = di->tracker_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(di->trackers_filtered));
2801 g_object_unref(di->trackers_filtered);
2802 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(v), FALSE);
2803 g_signal_connect(v, "button-press-event", G_CALLBACK(on_tree_view_button_pressed), NULL);
2804 g_signal_connect(v, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL);
2805
2806 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(v));
2807 g_signal_connect(sel, "changed", G_CALLBACK(on_tracker_list_selection_changed), di);
2808
2809 c = gtk_tree_view_column_new();
2810 gtk_tree_view_column_set_title(c, _("Trackers"));
2811 gtk_tree_view_append_column(GTK_TREE_VIEW(v), c);
2812
2813 r = gtk_cell_renderer_pixbuf_new();
2814 g_object_set(r, "width", 20 + (GUI_PAD_SMALL * 2), "xpad", GUI_PAD_SMALL, "ypad", pad, "yalign", 0.0F, NULL);
2815 gtk_tree_view_column_pack_start(c, r, FALSE);
2816 gtk_tree_view_column_add_attribute(c, r, "pixbuf", TRACKER_COL_FAVICON);
2817
2818 r = gtk_cell_renderer_text_new();
2819 g_object_set(G_OBJECT(r), "ellipsize", PANGO_ELLIPSIZE_END, "xpad", GUI_PAD_SMALL, "ypad", pad, NULL);
2820 gtk_tree_view_column_pack_start(c, r, TRUE);
2821 gtk_tree_view_column_add_attribute(c, r, "markup", TRACKER_COL_TEXT);
2822
2823 sw = gtk_scrolled_window_new(NULL, NULL);
2824 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
2825 gtk_container_add(GTK_CONTAINER(sw), v);
2826 w = gtk_frame_new(NULL);
2827 gtk_frame_set_shadow_type(GTK_FRAME(w), GTK_SHADOW_IN);
2828 gtk_container_add(GTK_CONTAINER(w), sw);
2829
2830 gtk_box_pack_start(GTK_BOX(hbox), w, TRUE, TRUE, 0);
2831
2832 v = gtk_box_new(GTK_ORIENTATION_VERTICAL, GUI_PAD);
2833
2834 w = gtk_button_new_with_mnemonic(_("_Add"));
2835 di->add_tracker_button = w;
2836 g_signal_connect(w, "clicked", G_CALLBACK(on_tracker_list_add_button_clicked), di);
2837 gtk_box_pack_start(GTK_BOX(v), w, FALSE, FALSE, 0);
2838
2839 w = gtk_button_new_with_mnemonic(_("_Edit"));
2840 gtk_button_set_image(GTK_BUTTON(w), gtk_image_new_from_icon_name(GTK_STOCK_EDIT, GTK_ICON_SIZE_BUTTON));
2841 g_signal_connect(w, "clicked", G_CALLBACK(on_edit_trackers), di);
2842 di->edit_trackers_button = w;
2843 gtk_box_pack_start(GTK_BOX(v), w, FALSE, FALSE, 0);
2844
2845 w = gtk_button_new_with_mnemonic(_("_Remove"));
2846 di->remove_tracker_button = w;
2847 g_signal_connect(w, "clicked", G_CALLBACK(on_tracker_list_remove_button_clicked), di);
2848 gtk_box_pack_start(GTK_BOX(v), w, FALSE, FALSE, 0);
2849
2850 gtk_box_pack_start(GTK_BOX(hbox), v, FALSE, FALSE, 0);
2851
2852 gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
2853
2854 w = gtk_check_button_new_with_mnemonic(_("Show _more details"));
2855 di->scrape_check = w;
2856 b = gtr_pref_flag_get(TR_KEY_show_tracker_scrapes);
2857 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), b);
2858 g_signal_connect(w, "toggled", G_CALLBACK(onScrapeToggled), di);
2859 gtk_box_pack_start(GTK_BOX(vbox), w, FALSE, FALSE, 0);
2860
2861 w = gtk_check_button_new_with_mnemonic(_("Show _backup trackers"));
2862 di->all_check = w;
2863 b = gtr_pref_flag_get(TR_KEY_show_backup_trackers);
2864 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), b);
2865 g_signal_connect(w, "toggled", G_CALLBACK(onBackupToggled), di);
2866 gtk_box_pack_start(GTK_BOX(vbox), w, FALSE, FALSE, 0);
2867
2868 return vbox;
2869 }
2870
2871 /****
2872 ***** DIALOG
2873 ****/
2874
refresh(struct DetailsImpl * di)2875 static void refresh(struct DetailsImpl* di)
2876 {
2877 int n;
2878 tr_torrent** torrents = getTorrents(di, &n);
2879
2880 refreshInfo(di, torrents, n);
2881 refreshPeers(di, torrents, n);
2882 refreshTracker(di, torrents, n);
2883 refreshOptions(di, torrents, n);
2884
2885 if (n == 0)
2886 {
2887 gtk_dialog_response(GTK_DIALOG(di->dialog), GTK_RESPONSE_CLOSE);
2888 }
2889
2890 g_free(torrents);
2891 }
2892
periodic_refresh(gpointer data)2893 static gboolean periodic_refresh(gpointer data)
2894 {
2895 refresh(data);
2896
2897 return G_SOURCE_CONTINUE;
2898 }
2899
on_details_window_size_allocated(GtkWidget * gtk_window,GtkAllocation * alloc UNUSED,gpointer gdata UNUSED)2900 static void on_details_window_size_allocated(GtkWidget* gtk_window, GtkAllocation* alloc UNUSED, gpointer gdata UNUSED)
2901 {
2902 GdkWindow* gdk_window = gtk_widget_get_window(gtk_window);
2903 int w, h;
2904 gtk_window_get_size(GTK_WINDOW(gtk_window), &w, &h);
2905 gtr_pref_int_set(TR_KEY_details_window_width, w);
2906 gtr_pref_int_set(TR_KEY_details_window_height, h);
2907 }
2908
details_free(gpointer gdata)2909 static void details_free(gpointer gdata)
2910 {
2911 struct DetailsImpl* data = gdata;
2912 g_source_remove(data->periodic_refresh_tag);
2913 g_hash_table_destroy(data->tracker_hash);
2914 g_hash_table_destroy(data->webseed_hash);
2915 g_hash_table_destroy(data->peer_hash);
2916 g_string_free(data->gstr, TRUE);
2917 g_slist_free(data->ids);
2918 g_free(data);
2919 }
2920
gtr_torrent_details_dialog_new(GtkWindow * parent,TrCore * core)2921 GtkWidget* gtr_torrent_details_dialog_new(GtkWindow* parent, TrCore* core)
2922 {
2923 GtkWidget* d;
2924 GtkWidget* n;
2925 GtkWidget* v;
2926 GtkWidget* w;
2927 GtkWidget* l;
2928 struct DetailsImpl* di = g_new0(struct DetailsImpl, 1);
2929
2930 /* one-time setup */
2931 if (ARG_KEY == 0)
2932 {
2933 ARG_KEY = g_quark_from_static_string("tr-arg-key");
2934 DETAILS_KEY = g_quark_from_static_string("tr-details-data-key");
2935 TORRENT_ID_KEY = g_quark_from_static_string("tr-torrent-id-key");
2936 TEXT_BUFFER_KEY = g_quark_from_static_string("tr-text-buffer-key");
2937 URL_ENTRY_KEY = g_quark_from_static_string("tr-url-entry-key");
2938 }
2939
2940 /* create the dialog */
2941 di->core = core;
2942 di->gstr = g_string_new(NULL);
2943 d = gtk_dialog_new_with_buttons(NULL, parent, 0, GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL);
2944 di->dialog = d;
2945 gtk_window_set_role(GTK_WINDOW(d), "tr-info");
2946
2947 /* return saved window size */
2948 gtk_window_resize(d, gtr_pref_int_get(TR_KEY_details_window_width), gtr_pref_int_get(TR_KEY_details_window_height));
2949 g_signal_connect(d, "size-allocate", G_CALLBACK(on_details_window_size_allocated), NULL);
2950
2951 g_signal_connect_swapped(d, "response", G_CALLBACK(gtk_widget_destroy), d);
2952 gtk_container_set_border_width(GTK_CONTAINER(d), GUI_PAD);
2953 g_object_set_qdata_full(G_OBJECT(d), DETAILS_KEY, di, details_free);
2954
2955 n = gtk_notebook_new();
2956 gtk_container_set_border_width(GTK_CONTAINER(n), GUI_PAD);
2957
2958 w = info_page_new(di);
2959 l = gtk_label_new(_("Information"));
2960 gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l);
2961
2962 w = peer_page_new(di);
2963 l = gtk_label_new(_("Peers"));
2964 gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l);
2965
2966 w = tracker_page_new(di);
2967 l = gtk_label_new(_("Trackers"));
2968 gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l);
2969
2970 v = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
2971 di->file_list = gtr_file_list_new(core, 0);
2972 di->file_label = gtk_label_new(_("File listing not available for combined torrent properties"));
2973 gtk_box_pack_start(GTK_BOX(v), di->file_list, TRUE, TRUE, 0);
2974 gtk_box_pack_start(GTK_BOX(v), di->file_label, TRUE, TRUE, 0);
2975 gtk_container_set_border_width(GTK_CONTAINER(v), GUI_PAD_BIG);
2976 l = gtk_label_new(_("Files"));
2977 gtk_notebook_append_page(GTK_NOTEBOOK(n), v, l);
2978
2979 w = options_page_new(di);
2980 l = gtk_label_new(_("Options"));
2981 gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l);
2982
2983 gtr_dialog_set_content(GTK_DIALOG(d), n);
2984 di->periodic_refresh_tag = gdk_threads_add_timeout_seconds(SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS, periodic_refresh, di);
2985 return d;
2986 }
2987
gtr_torrent_details_dialog_set_torrents(GtkWidget * w,GSList * ids)2988 void gtr_torrent_details_dialog_set_torrents(GtkWidget* w, GSList* ids)
2989 {
2990 char title[256];
2991 int const len = g_slist_length(ids);
2992 struct DetailsImpl* di = g_object_get_qdata(G_OBJECT(w), DETAILS_KEY);
2993
2994 g_slist_free(di->ids);
2995 di->ids = g_slist_copy(ids);
2996
2997 if (len == 1)
2998 {
2999 int const id = GPOINTER_TO_INT(ids->data);
3000 tr_torrent* tor = gtr_core_find_torrent(di->core, id);
3001 tr_info const* inf = tr_torrentInfo(tor);
3002 g_snprintf(title, sizeof(title), _("%s Properties"), inf->name);
3003
3004 gtr_file_list_set_torrent(di->file_list, id);
3005 gtk_widget_show(di->file_list);
3006 gtk_widget_hide(di->file_label);
3007 }
3008 else
3009 {
3010 gtr_file_list_clear(di->file_list);
3011 gtk_widget_hide(di->file_list);
3012 gtk_widget_show(di->file_label);
3013 g_snprintf(title, sizeof(title), _("%'d Torrent Properties"), len);
3014 }
3015
3016 gtk_window_set_title(GTK_WINDOW(w), title);
3017
3018 refresh(di);
3019 }
3020