/* * This file Copyright (C) 2007-2014 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. * */ #include /* INT_MAX */ #include #include /* sscanf() */ #include /* abort() */ #include #include #include #include /* tr_free */ #include "actions.h" #include "conf.h" #include "details.h" #include "favicon.h" /* gtr_get_favicon() */ #include "file-list.h" #include "hig.h" #include "tr-prefs.h" #include "util.h" static GQuark ARG_KEY = 0; static GQuark DETAILS_KEY = 0; static GQuark TORRENT_ID_KEY = 0; static GQuark TEXT_BUFFER_KEY = 0; static GQuark URL_ENTRY_KEY = 0; struct DetailsImpl { GtkWidget* dialog; GtkWidget* honor_limits_check; GtkWidget* up_limited_check; GtkWidget* up_limit_sping; GtkWidget* down_limited_check; GtkWidget* down_limit_spin; GtkWidget* bandwidth_combo; GtkWidget* ratio_combo; GtkWidget* ratio_spin; GtkWidget* idle_combo; GtkWidget* idle_spin; GtkWidget* max_peers_spin; gulong honor_limits_check_tag; gulong up_limited_check_tag; gulong down_limited_check_tag; gulong down_limit_spin_tag; gulong up_limit_spin_tag; gulong bandwidth_combo_tag; gulong ratio_combo_tag; gulong ratio_spin_tag; gulong idle_combo_tag; gulong idle_spin_tag; gulong max_peers_spin_tag; GtkWidget* size_lb; GtkWidget* state_lb; GtkWidget* have_lb; GtkWidget* dl_lb; GtkWidget* ul_lb; GtkWidget* error_lb; GtkWidget* date_started_lb; GtkWidget* eta_lb; GtkWidget* last_activity_lb; GtkWidget* hash_lb; GtkWidget* privacy_lb; GtkWidget* origin_lb; GtkWidget* destination_lb; GtkTextBuffer* comment_buffer; GHashTable* peer_hash; GHashTable* webseed_hash; GtkListStore* peer_store; GtkListStore* webseed_store; GtkWidget* webseed_view; GtkWidget* peer_view; GtkWidget* more_peer_details_check; GtkListStore* tracker_store; GHashTable* tracker_hash; GtkTreeModel* trackers_filtered; GtkWidget* add_tracker_button; GtkWidget* edit_trackers_button; GtkWidget* remove_tracker_button; GtkWidget* tracker_view; GtkWidget* scrape_check; GtkWidget* all_check; GtkWidget* file_list; GtkWidget* file_label; GSList* ids; TrCore* core; guint periodic_refresh_tag; GString* gstr; }; static tr_torrent** getTorrents(struct DetailsImpl* d, int* setmeCount) { int torrentCount = 0; int const n = g_slist_length(d->ids); tr_torrent** torrents = g_new(tr_torrent*, n); for (GSList* l = d->ids; l != NULL; l = l->next) { if ((torrents[torrentCount] = gtr_core_find_torrent(d->core, GPOINTER_TO_INT(l->data))) != NULL) { ++torrentCount; } } *setmeCount = torrentCount; return torrents; } /**** ***** ***** OPTIONS TAB ***** ****/ static void set_togglebutton_if_different(GtkWidget* w, gulong tag, gboolean value) { GtkToggleButton* toggle = GTK_TOGGLE_BUTTON(w); gboolean const currentValue = gtk_toggle_button_get_active(toggle); if (currentValue != value) { g_signal_handler_block(toggle, tag); gtk_toggle_button_set_active(toggle, value); g_signal_handler_unblock(toggle, tag); } } static void set_int_spin_if_different(GtkWidget* w, gulong tag, int value) { GtkSpinButton* spin = GTK_SPIN_BUTTON(w); int const currentValue = gtk_spin_button_get_value_as_int(spin); if (currentValue != value) { g_signal_handler_block(spin, tag); gtk_spin_button_set_value(spin, value); g_signal_handler_unblock(spin, tag); } } static void set_double_spin_if_different(GtkWidget* w, gulong tag, double value) { GtkSpinButton* spin = GTK_SPIN_BUTTON(w); double const currentValue = gtk_spin_button_get_value(spin); if ((int)(currentValue * 100) != (int)(value * 100)) { g_signal_handler_block(spin, tag); gtk_spin_button_set_value(spin, value); g_signal_handler_unblock(spin, tag); } } static void unset_combo(GtkWidget* w, gulong tag) { GtkComboBox* combobox = GTK_COMBO_BOX(w); g_signal_handler_block(combobox, tag); gtk_combo_box_set_active(combobox, -1); g_signal_handler_unblock(combobox, tag); } static void refreshOptions(struct DetailsImpl* di, tr_torrent** torrents, int n) { /*** **** Options Page ***/ /* honor_limits_check */ if (n != 0) { bool const baseline = tr_torrentUsesSessionLimits(torrents[0]); bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = baseline == tr_torrentUsesSessionLimits(torrents[i]); } if (is_uniform) { set_togglebutton_if_different(di->honor_limits_check, di->honor_limits_check_tag, baseline); } } /* down_limited_check */ if (n != 0) { bool const baseline = tr_torrentUsesSpeedLimit(torrents[0], TR_DOWN); bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = baseline == tr_torrentUsesSpeedLimit(torrents[i], TR_DOWN); } if (is_uniform) { set_togglebutton_if_different(di->down_limited_check, di->down_limited_check_tag, baseline); } } /* down_limit_spin */ if (n != 0) { unsigned int const baseline = tr_torrentGetSpeedLimit_KBps(torrents[0], TR_DOWN); bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = baseline == tr_torrentGetSpeedLimit_KBps(torrents[i], TR_DOWN); } if (is_uniform) { set_int_spin_if_different(di->down_limit_spin, di->down_limit_spin_tag, baseline); } } /* up_limited_check */ if (n != 0) { bool const baseline = tr_torrentUsesSpeedLimit(torrents[0], TR_UP); bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = baseline == tr_torrentUsesSpeedLimit(torrents[i], TR_UP); } if (is_uniform) { set_togglebutton_if_different(di->up_limited_check, di->up_limited_check_tag, baseline); } } /* up_limit_sping */ if (n != 0) { unsigned int const baseline = tr_torrentGetSpeedLimit_KBps(torrents[0], TR_UP); bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = baseline == tr_torrentGetSpeedLimit_KBps(torrents[i], TR_UP); } if (is_uniform) { set_int_spin_if_different(di->up_limit_sping, di->up_limit_spin_tag, baseline); } } /* bandwidth_combo */ if (n != 0) { int const baseline = tr_torrentGetPriority(torrents[0]); bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = baseline == tr_torrentGetPriority(torrents[i]); } if (is_uniform) { GtkWidget* w = di->bandwidth_combo; g_signal_handler_block(w, di->bandwidth_combo_tag); gtr_priority_combo_set_value(GTK_COMBO_BOX(w), baseline); g_signal_handler_unblock(w, di->bandwidth_combo_tag); } else { unset_combo(di->bandwidth_combo, di->bandwidth_combo_tag); } } /* ratio_combo */ if (n != 0) { int const baseline = tr_torrentGetRatioMode(torrents[0]); bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = baseline == (int)tr_torrentGetRatioMode(torrents[i]); } if (is_uniform) { GtkWidget* w = di->ratio_combo; g_signal_handler_block(w, di->ratio_combo_tag); gtr_combo_box_set_active_enum(GTK_COMBO_BOX(w), baseline); gtr_widget_set_visible(di->ratio_spin, baseline == TR_RATIOLIMIT_SINGLE); g_signal_handler_unblock(w, di->ratio_combo_tag); } } /* ratio_spin */ if (n != 0) { double const baseline = tr_torrentGetRatioLimit(torrents[0]); set_double_spin_if_different(di->ratio_spin, di->ratio_spin_tag, baseline); } /* idle_combo */ if (n != 0) { int const baseline = tr_torrentGetIdleMode(torrents[0]); bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = baseline == (int)tr_torrentGetIdleMode(torrents[i]); } if (is_uniform) { GtkWidget* w = di->idle_combo; g_signal_handler_block(w, di->idle_combo_tag); gtr_combo_box_set_active_enum(GTK_COMBO_BOX(w), baseline); gtr_widget_set_visible(di->idle_spin, baseline == TR_IDLELIMIT_SINGLE); g_signal_handler_unblock(w, di->idle_combo_tag); } } /* idle_spin */ if (n != 0) { int const baseline = tr_torrentGetIdleLimit(torrents[0]); set_int_spin_if_different(di->idle_spin, di->idle_spin_tag, baseline); } /* max_peers_spin */ if (n != 0) { int const baseline = tr_torrentGetPeerLimit(torrents[0]); set_int_spin_if_different(di->max_peers_spin, di->max_peers_spin_tag, baseline); } } static void torrent_set_bool(struct DetailsImpl* di, tr_quark const key, gboolean value) { tr_variant top; tr_variant* args; tr_variant* ids; tr_variantInitDict(&top, 2); tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set"); args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2); tr_variantDictAddBool(args, key, value); ids = tr_variantDictAddList(args, TR_KEY_ids, g_slist_length(di->ids)); for (GSList* l = di->ids; l != NULL; l = l->next) { tr_variantListAddInt(ids, GPOINTER_TO_INT(l->data)); } gtr_core_exec(di->core, &top); tr_variantFree(&top); } static void torrent_set_int(struct DetailsImpl* di, tr_quark const key, int value) { tr_variant top; tr_variant* args; tr_variant* ids; tr_variantInitDict(&top, 2); tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set"); args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2); tr_variantDictAddInt(args, key, value); ids = tr_variantDictAddList(args, TR_KEY_ids, g_slist_length(di->ids)); for (GSList* l = di->ids; l != NULL; l = l->next) { tr_variantListAddInt(ids, GPOINTER_TO_INT(l->data)); } gtr_core_exec(di->core, &top); tr_variantFree(&top); } static void torrent_set_real(struct DetailsImpl* di, tr_quark const key, double value) { tr_variant top; tr_variant* args; tr_variant* ids; tr_variantInitDict(&top, 2); tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set"); args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2); tr_variantDictAddReal(args, key, value); ids = tr_variantDictAddList(args, TR_KEY_ids, g_slist_length(di->ids)); for (GSList* l = di->ids; l != NULL; l = l->next) { tr_variantListAddInt(ids, GPOINTER_TO_INT(l->data)); } gtr_core_exec(di->core, &top); tr_variantFree(&top); } static void up_speed_toggled_cb(GtkToggleButton* tb, gpointer d) { torrent_set_bool(d, TR_KEY_uploadLimited, gtk_toggle_button_get_active(tb)); } static void down_speed_toggled_cb(GtkToggleButton* tb, gpointer d) { torrent_set_bool(d, TR_KEY_downloadLimited, gtk_toggle_button_get_active(tb)); } static void global_speed_toggled_cb(GtkToggleButton* tb, gpointer d) { torrent_set_bool(d, TR_KEY_honorsSessionLimits, gtk_toggle_button_get_active(tb)); } static void up_speed_spun_cb(GtkSpinButton* s, struct DetailsImpl* di) { torrent_set_int(di, TR_KEY_uploadLimit, gtk_spin_button_get_value_as_int(s)); } static void down_speed_spun_cb(GtkSpinButton* s, struct DetailsImpl* di) { torrent_set_int(di, TR_KEY_downloadLimit, gtk_spin_button_get_value_as_int(s)); } static void idle_spun_cb(GtkSpinButton* s, struct DetailsImpl* di) { torrent_set_int(di, TR_KEY_seedIdleLimit, gtk_spin_button_get_value_as_int(s)); } static void ratio_spun_cb(GtkSpinButton* s, struct DetailsImpl* di) { torrent_set_real(di, TR_KEY_seedRatioLimit, gtk_spin_button_get_value(s)); } static void max_peers_spun_cb(GtkSpinButton* s, struct DetailsImpl* di) { torrent_set_int(di, TR_KEY_peer_limit, gtk_spin_button_get_value(s)); } static void onPriorityChanged(GtkComboBox* combo_box, struct DetailsImpl* di) { tr_priority_t const priority = gtr_priority_combo_get_value(combo_box); torrent_set_int(di, TR_KEY_bandwidthPriority, priority); } static GtkWidget* new_priority_combo(struct DetailsImpl* di) { GtkWidget* w = gtr_priority_combo_new(); di->bandwidth_combo_tag = g_signal_connect(w, "changed", G_CALLBACK(onPriorityChanged), di); return w; } static void refresh(struct DetailsImpl* di); static void onComboEnumChanged(GtkComboBox* combo_box, struct DetailsImpl* di) { tr_quark const key = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(combo_box), ARG_KEY)); torrent_set_int(di, key, gtr_combo_box_get_active_enum(combo_box)); refresh(di); } static GtkWidget* ratio_combo_new(void) { GtkWidget* w = gtr_combo_box_new_enum( _("Use global settings"), TR_RATIOLIMIT_GLOBAL, _("Seed regardless of ratio"), TR_RATIOLIMIT_UNLIMITED, _("Stop seeding at ratio:"), TR_RATIOLIMIT_SINGLE, NULL); g_object_set_qdata(G_OBJECT(w), ARG_KEY, GINT_TO_POINTER(TR_KEY_seedRatioMode)); return w; } static GtkWidget* idle_combo_new(void) { GtkWidget* w = gtr_combo_box_new_enum( _("Use global settings"), TR_IDLELIMIT_GLOBAL, _("Seed regardless of activity"), TR_IDLELIMIT_UNLIMITED, _("Stop seeding if idle for N minutes:"), TR_IDLELIMIT_SINGLE, NULL); g_object_set_qdata(G_OBJECT(w), ARG_KEY, GINT_TO_POINTER(TR_KEY_seedIdleMode)); return w; } static GtkWidget* options_page_new(struct DetailsImpl* d) { guint row; gulong tag; char buf[128]; GtkWidget* t; GtkWidget* w; GtkWidget* tb; GtkWidget* h; row = 0; t = hig_workarea_create(); hig_workarea_add_section_title(t, &row, _("Speed")); tb = hig_workarea_add_wide_checkbutton(t, &row, _("Honor global _limits"), 0); d->honor_limits_check = tb; tag = g_signal_connect(tb, "toggled", G_CALLBACK(global_speed_toggled_cb), d); d->honor_limits_check_tag = tag; g_snprintf(buf, sizeof(buf), _("Limit _download speed (%s):"), _(speed_K_str)); tb = gtk_check_button_new_with_mnemonic(buf); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(tb), FALSE); d->down_limited_check = tb; tag = g_signal_connect(tb, "toggled", G_CALLBACK(down_speed_toggled_cb), d); d->down_limited_check_tag = tag; w = gtk_spin_button_new_with_range(0, INT_MAX, 5); tag = g_signal_connect(w, "value-changed", G_CALLBACK(down_speed_spun_cb), d); d->down_limit_spin_tag = tag; hig_workarea_add_row_w(t, &row, tb, w, NULL); d->down_limit_spin = w; g_snprintf(buf, sizeof(buf), _("Limit _upload speed (%s):"), _(speed_K_str)); tb = gtk_check_button_new_with_mnemonic(buf); d->up_limited_check = tb; tag = g_signal_connect(tb, "toggled", G_CALLBACK(up_speed_toggled_cb), d); d->up_limited_check_tag = tag; w = gtk_spin_button_new_with_range(0, INT_MAX, 5); tag = g_signal_connect(w, "value-changed", G_CALLBACK(up_speed_spun_cb), d); d->up_limit_spin_tag = tag; hig_workarea_add_row_w(t, &row, tb, w, NULL); d->up_limit_sping = w; w = new_priority_combo(d); hig_workarea_add_row(t, &row, _("Torrent _priority:"), w, NULL); d->bandwidth_combo = w; hig_workarea_add_section_divider(t, &row); hig_workarea_add_section_title(t, &row, _("Seeding Limits")); h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD); w = d->ratio_combo = ratio_combo_new(); d->ratio_combo_tag = g_signal_connect(w, "changed", G_CALLBACK(onComboEnumChanged), d); gtk_box_pack_start(GTK_BOX(h), w, TRUE, TRUE, 0); w = d->ratio_spin = gtk_spin_button_new_with_range(0, 1000, .05); gtk_entry_set_width_chars(GTK_ENTRY(w), 7); d->ratio_spin_tag = g_signal_connect(w, "value-changed", G_CALLBACK(ratio_spun_cb), d); gtk_box_pack_start(GTK_BOX(h), w, FALSE, FALSE, 0); hig_workarea_add_row(t, &row, _("_Ratio:"), h, NULL); h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD); w = d->idle_combo = idle_combo_new(); d->idle_combo_tag = g_signal_connect(w, "changed", G_CALLBACK(onComboEnumChanged), d); gtk_box_pack_start(GTK_BOX(h), w, TRUE, TRUE, 0); w = d->idle_spin = gtk_spin_button_new_with_range(1, 40320, 5); d->idle_spin_tag = g_signal_connect(w, "value-changed", G_CALLBACK(idle_spun_cb), d); gtk_box_pack_start(GTK_BOX(h), w, FALSE, FALSE, 0); hig_workarea_add_row(t, &row, _("_Idle:"), h, NULL); hig_workarea_add_section_divider(t, &row); hig_workarea_add_section_title(t, &row, _("Peer Connections")); w = gtk_spin_button_new_with_range(1, 3000, 5); hig_workarea_add_row(t, &row, _("_Maximum peers:"), w, w); tag = g_signal_connect(w, "value-changed", G_CALLBACK(max_peers_spun_cb), d); d->max_peers_spin = w; d->max_peers_spin_tag = tag; return t; } /**** ***** ***** INFO TAB ***** ****/ static char const* activityString(int activity, bool finished) { switch (activity) { case TR_STATUS_CHECK_WAIT: return _("Queued for verification"); case TR_STATUS_CHECK: return _("Verifying local data"); case TR_STATUS_DOWNLOAD_WAIT: return _("Queued for download"); case TR_STATUS_DOWNLOAD: return C_("Verb", "Downloading"); case TR_STATUS_SEED_WAIT: return _("Queued for seeding"); case TR_STATUS_SEED: return C_("Verb", "Seeding"); case TR_STATUS_STOPPED: return finished ? _("Finished") : _("Paused"); } return ""; } /* Only call gtk_text_buffer_set_text () if the new text differs from the old. * This way if the user has text selected, refreshing won't deselect it */ static void gtr_text_buffer_set_text(GtkTextBuffer* b, char const* str) { char* old_str; GtkTextIter start; GtkTextIter end; if (str == NULL) { str = ""; } gtk_text_buffer_get_bounds(b, &start, &end); old_str = gtk_text_buffer_get_text(b, &start, &end, FALSE); if (old_str == NULL || g_strcmp0(old_str, str) != 0) { gtk_text_buffer_set_text(b, str, -1); } g_free(old_str); } static char* get_short_date_string(time_t t) { char buf[64]; struct tm tm; if (t == 0) { return g_strdup(_("N/A")); } tr_localtime_r(&t, &tm); strftime(buf, sizeof(buf), "%d %b %Y", &tm); return g_locale_to_utf8(buf, -1, NULL, NULL, NULL); } static void refreshInfo(struct DetailsImpl* di, tr_torrent** torrents, int n) { char const* str; char const* mixed = _("Mixed"); char const* no_torrent = _("No Torrents Selected"); char const* stateString; char buf[512]; uint64_t sizeWhenDone = 0; tr_stat const** stats = g_new(tr_stat const*, n); tr_info const** infos = g_new(tr_info const*, n); for (int i = 0; i < n; ++i) { stats[i] = tr_torrentStatCached(torrents[i]); infos[i] = tr_torrentInfo(torrents[i]); } /* privacy_lb */ if (n <= 0) { str = no_torrent; } else { bool const baseline = infos[0]->isPrivate; bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = baseline == infos[i]->isPrivate; } if (is_uniform) { str = baseline ? _("Private to this tracker -- DHT and PEX disabled") : _("Public torrent"); } else { str = mixed; } } gtr_label_set_text(GTK_LABEL(di->privacy_lb), str); /* origin_lb */ if (n <= 0) { str = no_torrent; } else { char const* creator = infos[0]->creator != NULL ? infos[0]->creator : ""; time_t const date = infos[0]->dateCreated; char* datestr = get_short_date_string(date); gboolean mixed_creator = FALSE; gboolean mixed_date = FALSE; for (int i = 1; i < n; ++i) { mixed_creator |= g_strcmp0(creator, infos[i]->creator != NULL ? infos[i]->creator : "") != 0; mixed_date |= date != infos[i]->dateCreated; } gboolean const empty_creator = tr_str_is_empty(creator); gboolean const empty_date = date == 0; if (mixed_date || mixed_creator) { str = mixed; } else if (empty_date && empty_creator) { str = _("N/A"); } else { if (empty_date && !empty_creator) { g_snprintf(buf, sizeof(buf), _("Created by %1$s"), creator); } else if (empty_creator && !empty_date) { g_snprintf(buf, sizeof(buf), _("Created on %1$s"), datestr); } else { g_snprintf(buf, sizeof(buf), _("Created by %1$s on %2$s"), creator, datestr); } str = buf; } g_free(datestr); } gtr_label_set_text(GTK_LABEL(di->origin_lb), str); /* comment_buffer */ if (n <= 0) { str = ""; } else { char const* baseline = infos[0]->comment != NULL ? infos[0]->comment : ""; bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = g_strcmp0(baseline, infos[i]->comment != NULL ? infos[i]->comment : "") == 0; } str = is_uniform ? baseline : mixed; } gtr_text_buffer_set_text(di->comment_buffer, str); /* destination_lb */ if (n <= 0) { str = no_torrent; } else { char const* baseline = tr_torrentGetDownloadDir(torrents[0]); bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = g_strcmp0(baseline, tr_torrentGetDownloadDir(torrents[i])) == 0; } str = is_uniform ? baseline : mixed; } gtr_label_set_text(GTK_LABEL(di->destination_lb), str); /* state_lb */ if (n <= 0) { str = no_torrent; } else { tr_torrent_activity const activity = stats[0]->activity; bool is_uniform = true; bool allFinished = stats[0]->finished; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = activity == stats[i]->activity; if (!stats[i]->finished) { allFinished = false; } } str = is_uniform ? activityString(activity, allFinished) : mixed; } stateString = str; gtr_label_set_text(GTK_LABEL(di->state_lb), str); /* date started */ if (n <= 0) { str = no_torrent; } else { time_t const baseline = stats[0]->startDate; bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = baseline == stats[i]->startDate; } if (!is_uniform) { str = mixed; } else if (baseline <= 0 || stats[0]->activity == TR_STATUS_STOPPED) { str = stateString; } else { str = tr_strltime(buf, time(NULL) - baseline, sizeof(buf)); } } gtr_label_set_text(GTK_LABEL(di->date_started_lb), str); /* eta */ if (n <= 0) { str = no_torrent; } else { int const baseline = stats[0]->eta; bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = baseline == stats[i]->eta; } if (!is_uniform) { str = mixed; } else if (baseline < 0) { str = _("Unknown"); } else { str = tr_strltime(buf, baseline, sizeof(buf)); } } gtr_label_set_text(GTK_LABEL(di->eta_lb), str); /* size_lb */ { char sizebuf[128]; uint64_t size = 0; int pieces = 0; int32_t pieceSize = 0; for (int i = 0; i < n; ++i) { size += infos[i]->totalSize; pieces += infos[i]->pieceCount; if (pieceSize == 0) { pieceSize = infos[i]->pieceSize; } else if (pieceSize != (int)infos[i]->pieceSize) { pieceSize = -1; } } tr_strlsize(sizebuf, size, sizeof(sizebuf)); if (size == 0) { str = ""; } else if (pieceSize >= 0) { char piecebuf[128]; tr_formatter_mem_B(piecebuf, pieceSize, sizeof(piecebuf)); g_snprintf(buf, sizeof(buf), ngettext("%1$s (%2$'d piece @ %3$s)", "%1$s (%2$'d pieces @ %3$s)", pieces), sizebuf, pieces, piecebuf); str = buf; } else { g_snprintf(buf, sizeof(buf), ngettext("%1$s (%2$'d piece)", "%1$s (%2$'d pieces)", pieces), sizebuf, pieces); str = buf; } gtr_label_set_text(GTK_LABEL(di->size_lb), str); } /* have_lb */ if (n <= 0) { str = no_torrent; } else { uint64_t leftUntilDone = 0; uint64_t haveUnchecked = 0; uint64_t haveValid = 0; uint64_t available = 0; for (int i = 0; i < n; ++i) { tr_stat const* st = stats[i]; haveUnchecked += st->haveUnchecked; haveValid += st->haveValid; sizeWhenDone += st->sizeWhenDone; leftUntilDone += st->leftUntilDone; available += st->sizeWhenDone - st->leftUntilDone + st->haveUnchecked + st->desiredAvailable; } { char buf2[32]; char unver[64]; char total[64]; char avail[32]; double const d = sizeWhenDone != 0 ? (100.0 * available) / sizeWhenDone : 0; double const ratio = 100.0 * (sizeWhenDone != 0 ? (haveValid + haveUnchecked) / (double)sizeWhenDone : 1); tr_strlpercent(avail, d, sizeof(avail)); tr_strlpercent(buf2, ratio, sizeof(buf2)); tr_strlsize(total, haveUnchecked + haveValid, sizeof(total)); tr_strlsize(unver, haveUnchecked, sizeof(unver)); if (haveUnchecked == 0 && leftUntilDone == 0) { g_snprintf(buf, sizeof(buf), _("%1$s (%2$s%%)"), total, buf2); } else if (haveUnchecked == 0) { g_snprintf(buf, sizeof(buf), _("%1$s (%2$s%% of %3$s%% Available)"), total, buf2, avail); } else { g_snprintf(buf, sizeof(buf), _("%1$s (%2$s%% of %3$s%% Available); %4$s Unverified"), total, buf2, avail, unver); } str = buf; } } gtr_label_set_text(GTK_LABEL(di->have_lb), str); /* dl_lb */ if (n <= 0) { str = no_torrent; } else { char dbuf[64]; char fbuf[64]; uint64_t d = 0; uint64_t f = 0; for (int i = 0; i < n; ++i) { d += stats[i]->downloadedEver; f += stats[i]->corruptEver; } tr_strlsize(dbuf, d, sizeof(dbuf)); tr_strlsize(fbuf, f, sizeof(fbuf)); if (f != 0) { g_snprintf(buf, sizeof(buf), _("%1$s (+%2$s corrupt)"), dbuf, fbuf); } else { tr_strlcpy(buf, dbuf, sizeof(buf)); } str = buf; } gtr_label_set_text(GTK_LABEL(di->dl_lb), str); /* ul_lb */ if (n <= 0) { str = no_torrent; } else { char upstr[64]; char ratiostr[64]; uint64_t up = 0; uint64_t down = 0; for (int i = 0; i < n; ++i) { up += stats[i]->uploadedEver; down += stats[i]->downloadedEver; } tr_strlsize(upstr, up, sizeof(upstr)); tr_strlratio(ratiostr, tr_getRatio(up, down), sizeof(ratiostr)); g_snprintf(buf, sizeof(buf), _("%s (Ratio: %s)"), upstr, ratiostr); str = buf; } gtr_label_set_text(GTK_LABEL(di->ul_lb), str); /* hash_lb */ if (n <= 0) { str = no_torrent; } else if (n == 1) { str = infos[0]->hashString; } else { str = mixed; } gtr_label_set_text(GTK_LABEL(di->hash_lb), str); /* error */ if (n <= 0) { str = no_torrent; } else { char const* baseline = stats[0]->errorString; bool is_uniform = true; for (int i = 1; is_uniform && i < n; ++i) { is_uniform = g_strcmp0(baseline, stats[i]->errorString) == 0; } str = is_uniform ? baseline : mixed; } if (tr_str_is_empty(str)) { str = _("No errors"); } gtr_label_set_text(GTK_LABEL(di->error_lb), str); /* activity date */ if (n <= 0) { str = no_torrent; } else { time_t latest = 0; for (int i = 0; i < n; ++i) { if (latest < stats[i]->activityDate) { latest = stats[i]->activityDate; } } if (latest <= 0) { str = _("Never"); } else { int const period = time(NULL) - latest; if (period < 5) { tr_strlcpy(buf, _("Active now"), sizeof(buf)); } else { char tbuf[128]; tr_strltime(tbuf, period, sizeof(tbuf)); g_snprintf(buf, sizeof(buf), _("%1$s ago"), tbuf); } str = buf; } } gtr_label_set_text(GTK_LABEL(di->last_activity_lb), str); g_free(stats); g_free(infos); } static GtkWidget* info_page_new(struct DetailsImpl* di) { guint row = 0; GtkTextBuffer* b; GtkWidget* l; GtkWidget* w; GtkWidget* fr; GtkWidget* sw; GtkWidget* t = hig_workarea_create(); hig_workarea_add_section_title(t, &row, _("Activity")); /* size */ l = di->size_lb = gtk_label_new(NULL); gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE); hig_workarea_add_row(t, &row, _("Torrent size:"), l, NULL); /* have */ l = di->have_lb = gtk_label_new(NULL); gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE); hig_workarea_add_row(t, &row, _("Have:"), l, NULL); /* uploaded */ l = di->ul_lb = gtk_label_new(NULL); gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE); hig_workarea_add_row(t, &row, _("Uploaded:"), l, NULL); /* downloaded */ l = di->dl_lb = gtk_label_new(NULL); gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE); hig_workarea_add_row(t, &row, _("Downloaded:"), l, NULL); /* state */ l = di->state_lb = gtk_label_new(NULL); gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE); hig_workarea_add_row(t, &row, _("State:"), l, NULL); /* running for */ l = di->date_started_lb = gtk_label_new(NULL); gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE); hig_workarea_add_row(t, &row, _("Running time:"), l, NULL); /* eta */ l = di->eta_lb = gtk_label_new(NULL); gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE); hig_workarea_add_row(t, &row, _("Remaining time:"), l, NULL); /* last activity */ l = di->last_activity_lb = gtk_label_new(NULL); gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE); hig_workarea_add_row(t, &row, _("Last activity:"), l, NULL); /* error */ l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL); hig_workarea_add_row(t, &row, _("Error:"), l, NULL); di->error_lb = l; hig_workarea_add_section_divider(t, &row); hig_workarea_add_section_title(t, &row, _("Details")); /* destination */ l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL); hig_workarea_add_row(t, &row, _("Location:"), l, NULL); di->destination_lb = l; /* hash */ l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL); hig_workarea_add_row(t, &row, _("Hash:"), l, NULL); di->hash_lb = l; /* privacy */ l = gtk_label_new(NULL); gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE); hig_workarea_add_row(t, &row, _("Privacy:"), l, NULL); di->privacy_lb = l; /* origins */ l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL); hig_workarea_add_row(t, &row, _("Origin:"), l, NULL); di->origin_lb = l; /* comment */ b = di->comment_buffer = gtk_text_buffer_new(NULL); w = gtk_text_view_new_with_buffer(b); gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(w), GTK_WRAP_WORD); gtk_text_view_set_editable(GTK_TEXT_VIEW(w), FALSE); sw = gtk_scrolled_window_new(NULL, NULL); gtk_widget_set_size_request(sw, 350, 36); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_container_add(GTK_CONTAINER(sw), w); fr = gtk_frame_new(NULL); gtk_frame_set_shadow_type(GTK_FRAME(fr), GTK_SHADOW_IN); gtk_container_add(GTK_CONTAINER(fr), sw); w = hig_workarea_add_tall_row(t, &row, _("Comment:"), fr, NULL); g_object_set(w, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_START, NULL); hig_workarea_add_section_divider(t, &row); return t; } /**** ***** ***** PEERS TAB ***** ****/ enum { WEBSEED_COL_KEY, WEBSEED_COL_WAS_UPDATED, WEBSEED_COL_URL, WEBSEED_COL_DOWNLOAD_RATE_DOUBLE, WEBSEED_COL_DOWNLOAD_RATE_STRING, N_WEBSEED_COLS }; static char const* getWebseedColumnNames(int column) { switch (column) { case WEBSEED_COL_URL: return _("Web Seeds"); case WEBSEED_COL_DOWNLOAD_RATE_DOUBLE: case WEBSEED_COL_DOWNLOAD_RATE_STRING: return _("Down"); default: return ""; } } static GtkListStore* webseed_model_new(void) { return gtk_list_store_new(N_WEBSEED_COLS, G_TYPE_STRING, /* key */ G_TYPE_BOOLEAN, /* was-updated */ G_TYPE_STRING, /* url */ G_TYPE_DOUBLE, /* download rate double */ G_TYPE_STRING); /* download rate string */ } enum { PEER_COL_KEY, PEER_COL_WAS_UPDATED, PEER_COL_ADDRESS, PEER_COL_ADDRESS_COLLATED, PEER_COL_DOWNLOAD_RATE_DOUBLE, PEER_COL_DOWNLOAD_RATE_STRING, PEER_COL_UPLOAD_RATE_DOUBLE, PEER_COL_UPLOAD_RATE_STRING, PEER_COL_CLIENT, PEER_COL_PROGRESS, PEER_COL_UPLOAD_REQUEST_COUNT_INT, PEER_COL_UPLOAD_REQUEST_COUNT_STRING, PEER_COL_DOWNLOAD_REQUEST_COUNT_INT, PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING, PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT, PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING, PEER_COL_BLOCKS_UPLOADED_COUNT_INT, PEER_COL_BLOCKS_UPLOADED_COUNT_STRING, PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT, PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING, PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT, PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING, PEER_COL_ENCRYPTION_STOCK_ID, PEER_COL_FLAGS, PEER_COL_TORRENT_NAME, N_PEER_COLS }; static char const* getPeerColumnName(int column) { switch (column) { case PEER_COL_ADDRESS: return _("Address"); case PEER_COL_DOWNLOAD_RATE_STRING: case PEER_COL_DOWNLOAD_RATE_DOUBLE: return _("Down"); case PEER_COL_UPLOAD_RATE_STRING: case PEER_COL_UPLOAD_RATE_DOUBLE: return _("Up"); case PEER_COL_CLIENT: return _("Client"); case PEER_COL_PROGRESS: return _("%"); case PEER_COL_UPLOAD_REQUEST_COUNT_INT: case PEER_COL_UPLOAD_REQUEST_COUNT_STRING: return _("Up Reqs"); case PEER_COL_DOWNLOAD_REQUEST_COUNT_INT: case PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING: return _("Dn Reqs"); case PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT: case PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING: return _("Dn Blocks"); case PEER_COL_BLOCKS_UPLOADED_COUNT_INT: case PEER_COL_BLOCKS_UPLOADED_COUNT_STRING: return _("Up Blocks"); case PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT: case PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING: return _("We Cancelled"); case PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT: case PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING: return _("They Cancelled"); case PEER_COL_FLAGS: return _("Flags"); default: return ""; } } static GtkListStore* peer_store_new(void) { return gtk_list_store_new(N_PEER_COLS, G_TYPE_STRING, /* key */ G_TYPE_BOOLEAN, /* was-updated */ G_TYPE_STRING, /* address */ G_TYPE_STRING, /* collated address */ G_TYPE_DOUBLE, /* download speed int */ G_TYPE_STRING, /* download speed string */ G_TYPE_DOUBLE, /* upload speed int */ G_TYPE_STRING, /* upload speed string */ G_TYPE_STRING, /* client */ G_TYPE_INT, /* progress [0..100] */ G_TYPE_INT, /* upload request count int */ G_TYPE_STRING, /* upload request count string */ G_TYPE_INT, /* download request count int */ G_TYPE_STRING, /* download request count string */ G_TYPE_INT, /* # blocks downloaded int */ G_TYPE_STRING, /* # blocks downloaded string */ G_TYPE_INT, /* # blocks uploaded int */ G_TYPE_STRING, /* # blocks uploaded string */ G_TYPE_INT, /* # blocks cancelled by client int */ G_TYPE_STRING, /* # blocks cancelled by client string */ G_TYPE_INT, /* # blocks cancelled by peer int */ G_TYPE_STRING, /* # blocks cancelled by peer string */ G_TYPE_STRING, /* encryption stock id */ G_TYPE_STRING, /* flagString */ G_TYPE_STRING); /* torrent name */ } static void initPeerRow(GtkListStore* store, GtkTreeIter* iter, char const* key, char const* torrentName, tr_peer_stat const* peer) { g_return_if_fail(peer != NULL); char const* client = peer->client; if (client == NULL || g_strcmp0(client, "Unknown Client") == 0) { client = ""; } int q[4]; char collated_name[128]; if (sscanf(peer->addr, "%d.%d.%d.%d", q, q + 1, q + 2, q + 3) != 4) { g_strlcpy(collated_name, peer->addr, sizeof(collated_name)); } else { g_snprintf(collated_name, sizeof(collated_name), "%03d.%03d.%03d.%03d", q[0], q[1], q[2], q[3]); } gtk_list_store_set(store, iter, PEER_COL_ADDRESS, peer->addr, PEER_COL_ADDRESS_COLLATED, collated_name, PEER_COL_CLIENT, client, PEER_COL_ENCRYPTION_STOCK_ID, peer->isEncrypted ? "transmission-lock" : NULL, PEER_COL_KEY, key, PEER_COL_TORRENT_NAME, torrentName, -1); } static void refreshPeerRow(GtkListStore* store, GtkTreeIter* iter, tr_peer_stat const* peer) { char up_speed[64] = { '\0' }; char down_speed[64] = { '\0' }; char up_count[64] = { '\0' }; char down_count[64] = { '\0' }; char blocks_to_peer[64] = { '\0' }; char blocks_to_client[64] = { '\0' }; char cancelled_by_peer[64] = { '\0' }; char cancelled_by_client[64] = { '\0' }; g_return_if_fail(peer != NULL); if (peer->rateToPeer_KBps > 0.01) { tr_formatter_speed_KBps(up_speed, peer->rateToPeer_KBps, sizeof(up_speed)); } if (peer->rateToClient_KBps > 0) { tr_formatter_speed_KBps(down_speed, peer->rateToClient_KBps, sizeof(down_speed)); } if (peer->pendingReqsToPeer > 0) { g_snprintf(down_count, sizeof(down_count), "%d", peer->pendingReqsToPeer); } if (peer->pendingReqsToClient > 0) { g_snprintf(up_count, sizeof(down_count), "%d", peer->pendingReqsToClient); } if (peer->blocksToPeer > 0) { g_snprintf(blocks_to_peer, sizeof(blocks_to_peer), "%" PRIu32, peer->blocksToPeer); } if (peer->blocksToClient > 0) { g_snprintf(blocks_to_client, sizeof(blocks_to_client), "%" PRIu32, peer->blocksToClient); } if (peer->cancelsToPeer > 0) { g_snprintf(cancelled_by_client, sizeof(cancelled_by_client), "%" PRIu32, peer->cancelsToPeer); } if (peer->cancelsToClient > 0) { g_snprintf(cancelled_by_peer, sizeof(cancelled_by_peer), "%" PRIu32, peer->cancelsToClient); } gtk_list_store_set(store, iter, PEER_COL_PROGRESS, (int)(100.0 * peer->progress), PEER_COL_UPLOAD_REQUEST_COUNT_INT, peer->pendingReqsToClient, PEER_COL_UPLOAD_REQUEST_COUNT_STRING, up_count, PEER_COL_DOWNLOAD_REQUEST_COUNT_INT, peer->pendingReqsToPeer, PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING, down_count, PEER_COL_DOWNLOAD_RATE_DOUBLE, peer->rateToClient_KBps, PEER_COL_DOWNLOAD_RATE_STRING, down_speed, PEER_COL_UPLOAD_RATE_DOUBLE, peer->rateToPeer_KBps, PEER_COL_UPLOAD_RATE_STRING, up_speed, PEER_COL_FLAGS, peer->flagStr, PEER_COL_WAS_UPDATED, TRUE, PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT, (int)peer->blocksToClient, PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING, blocks_to_client, PEER_COL_BLOCKS_UPLOADED_COUNT_INT, (int)peer->blocksToPeer, PEER_COL_BLOCKS_UPLOADED_COUNT_STRING, blocks_to_peer, PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT, (int)peer->cancelsToPeer, PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING, cancelled_by_client, PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT, (int)peer->cancelsToClient, PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING, cancelled_by_peer, -1); } static void refreshPeerList(struct DetailsImpl* di, tr_torrent** torrents, int n) { int* peerCount; GtkTreeIter iter; GtkTreeModel* model; GHashTable* hash = di->peer_hash; GtkListStore* store = di->peer_store; struct tr_peer_stat** peers; /* step 1: get all the peers */ peers = g_new(struct tr_peer_stat*, n); peerCount = g_new(int, n); for (int i = 0; i < n; ++i) { peers[i] = tr_torrentPeers(torrents[i], &peerCount[i]); } /* step 2: mark all the peers in the list as not-updated */ model = GTK_TREE_MODEL(store); if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0)) { do { gtk_list_store_set(store, &iter, PEER_COL_WAS_UPDATED, FALSE, -1); } while (gtk_tree_model_iter_next(model, &iter)); } /* step 3: add any new peers */ for (int i = 0; i < n; ++i) { tr_torrent const* tor = torrents[i]; for (int j = 0; j < peerCount[i]; ++j) { char key[128]; tr_peer_stat const* s = &peers[i][j]; g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), s->addr); if (g_hash_table_lookup(hash, key) == NULL) { GtkTreePath* p; gtk_list_store_append(store, &iter); initPeerRow(store, &iter, key, tr_torrentName(tor), s); p = gtk_tree_model_get_path(model, &iter); g_hash_table_insert(hash, g_strdup(key), gtk_tree_row_reference_new(model, p)); gtk_tree_path_free(p); } } } /* step 4: update the peers */ for (int i = 0; i < n; ++i) { tr_torrent const* tor = torrents[i]; for (int j = 0; j < peerCount[i]; ++j) { char key[128]; GtkTreePath* p; GtkTreeRowReference* ref; tr_peer_stat const* s = &peers[i][j]; g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), s->addr); ref = g_hash_table_lookup(hash, key); p = gtk_tree_row_reference_get_path(ref); gtk_tree_model_get_iter(model, &iter, p); refreshPeerRow(store, &iter, s); gtk_tree_path_free(p); } } /* step 5: remove peers that have disappeared */ model = GTK_TREE_MODEL(store); if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0)) { gboolean more = TRUE; while (more) { gboolean b; gtk_tree_model_get(model, &iter, PEER_COL_WAS_UPDATED, &b, -1); if (b) { more = gtk_tree_model_iter_next(model, &iter); } else { char* key; gtk_tree_model_get(model, &iter, PEER_COL_KEY, &key, -1); g_hash_table_remove(hash, key); more = gtk_list_store_remove(store, &iter); g_free(key); } } } /* step 6: cleanup */ for (int i = 0; i < n; ++i) { tr_torrentPeersFree(peers[i], peerCount[i]); } tr_free(peers); tr_free(peerCount); } static void refreshWebseedList(struct DetailsImpl* di, tr_torrent** torrents, int n) { int total = 0; GtkTreeIter iter; GHashTable* hash = di->webseed_hash; GtkListStore* store = di->webseed_store; GtkTreeModel* model = GTK_TREE_MODEL(store); /* step 1: mark all webseeds as not-updated */ if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0)) { do { gtk_list_store_set(store, &iter, WEBSEED_COL_WAS_UPDATED, FALSE, -1); } while (gtk_tree_model_iter_next(model, &iter)); } /* step 2: add any new webseeds */ for (int i = 0; i < n; ++i) { tr_torrent const* tor = torrents[i]; tr_info const* inf = tr_torrentInfo(tor); total += inf->webseedCount; for (unsigned int j = 0; j < inf->webseedCount; ++j) { char key[256]; char const* url = inf->webseeds[j]; g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), url); if (g_hash_table_lookup(hash, key) == NULL) { GtkTreePath* p; gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, WEBSEED_COL_URL, url, WEBSEED_COL_KEY, key, -1); p = gtk_tree_model_get_path(model, &iter); g_hash_table_insert(hash, g_strdup(key), gtk_tree_row_reference_new(model, p)); gtk_tree_path_free(p); } } } /* step 3: update the webseeds */ for (int i = 0; i < n; ++i) { tr_torrent* tor = torrents[i]; tr_info const* inf = tr_torrentInfo(tor); double* speeds_KBps = tr_torrentWebSpeeds_KBps(tor); for (unsigned int j = 0; j < inf->webseedCount; ++j) { char buf[128]; char key[256]; GtkTreePath* p; GtkTreeRowReference* ref; char const* url = inf->webseeds[j]; g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), url); ref = g_hash_table_lookup(hash, key); p = gtk_tree_row_reference_get_path(ref); gtk_tree_model_get_iter(model, &iter, p); if (speeds_KBps[j] > 0) { tr_formatter_speed_KBps(buf, speeds_KBps[j], sizeof(buf)); } else { *buf = '\0'; } gtk_list_store_set(store, &iter, WEBSEED_COL_DOWNLOAD_RATE_DOUBLE, speeds_KBps[j], WEBSEED_COL_DOWNLOAD_RATE_STRING, buf, WEBSEED_COL_WAS_UPDATED, TRUE, -1); gtk_tree_path_free(p); } tr_free(speeds_KBps); } /* step 4: remove webseeds that have disappeared */ if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0)) { gboolean more = TRUE; while (more) { gboolean b; gtk_tree_model_get(model, &iter, WEBSEED_COL_WAS_UPDATED, &b, -1); if (b) { more = gtk_tree_model_iter_next(model, &iter); } else { char* key; gtk_tree_model_get(model, &iter, WEBSEED_COL_KEY, &key, -1); if (key != NULL) { g_hash_table_remove(hash, key); } more = gtk_list_store_remove(store, &iter); g_free(key); } } } /* most of the time there are no webseeds... don't waste space showing an empty list */ gtk_widget_set_visible(di->webseed_view, total > 0); } static void refreshPeers(struct DetailsImpl* di, tr_torrent** torrents, int n) { refreshPeerList(di, torrents, n); refreshWebseedList(di, torrents, n); } static gboolean onPeerViewQueryTooltip(GtkWidget* widget, gint x, gint y, gboolean keyboard_tip, GtkTooltip* tooltip, gpointer gdi) { GtkTreeIter iter; GtkTreeModel* model; gboolean show_tip = FALSE; if (gtk_tree_view_get_tooltip_context(GTK_TREE_VIEW(widget), &x, &y, keyboard_tip, &model, NULL, &iter)) { char* name = NULL; char* addr = NULL; char* markup = NULL; char* flagstr = NULL; struct DetailsImpl* di = gdi; GString* gstr = di->gstr; gtk_tree_model_get(model, &iter, PEER_COL_TORRENT_NAME, &name, PEER_COL_ADDRESS, &addr, PEER_COL_FLAGS, &flagstr, -1); g_string_truncate(gstr, 0); markup = g_markup_escape_text(name, -1); g_string_append_printf(gstr, "%s\n%s\n \n", markup, addr); g_free(markup); for (char const* pch = flagstr; !tr_str_is_empty(pch); ++pch) { char const* s = NULL; switch (*pch) { case 'O': s = _("Optimistic unchoke"); break; case 'D': s = _("Downloading from this peer"); break; case 'd': s = _("We would download from this peer if they would let us"); break; case 'U': s = _("Uploading to peer"); break; case 'u': s = _("We would upload to this peer if they asked"); break; case 'K': s = _("Peer has unchoked us, but we're not interested"); break; case '?': s = _("We unchoked this peer, but they're not interested"); break; case 'E': s = _("Encrypted connection"); break; case 'X': s = _("Peer was found through Peer Exchange (PEX)"); break; case 'H': s = _("Peer was found through DHT"); break; case 'I': s = _("Peer is an incoming connection"); break; case 'T': s = _("Peer is connected over µTP"); break; } if (s != NULL) { g_string_append_printf(gstr, "%c: %s\n", *pch, s); } } if (gstr->len != 0) /* remove the last linefeed */ { g_string_set_size(gstr, gstr->len - 1); } gtk_tooltip_set_markup(tooltip, gstr->str); g_free(flagstr); g_free(addr); g_free(name); show_tip = TRUE; } return show_tip; } static void setPeerViewColumns(GtkTreeView* peer_view) { int n; int view_columns[32]; GtkCellRenderer* r; GtkTreeViewColumn* c; bool const more = gtr_pref_flag_get(TR_KEY_show_extra_peer_details); n = 0; view_columns[n++] = PEER_COL_ENCRYPTION_STOCK_ID; view_columns[n++] = PEER_COL_UPLOAD_RATE_STRING; if (more) { view_columns[n++] = PEER_COL_UPLOAD_REQUEST_COUNT_STRING; } view_columns[n++] = PEER_COL_DOWNLOAD_RATE_STRING; if (more) { view_columns[n++] = PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING; } if (more) { view_columns[n++] = PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING; } if (more) { view_columns[n++] = PEER_COL_BLOCKS_UPLOADED_COUNT_STRING; } if (more) { view_columns[n++] = PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING; } if (more) { view_columns[n++] = PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING; } view_columns[n++] = PEER_COL_PROGRESS; view_columns[n++] = PEER_COL_FLAGS; view_columns[n++] = PEER_COL_ADDRESS; view_columns[n++] = PEER_COL_CLIENT; /* remove any existing columns */ while ((c = gtk_tree_view_get_column(peer_view, 0)) != NULL) { gtk_tree_view_remove_column(peer_view, c); } for (int i = 0; i < n; ++i) { int const col = view_columns[i]; char const* t = getPeerColumnName(col); int sort_col = col; switch (col) { case PEER_COL_ADDRESS: r = gtk_cell_renderer_text_new(); c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL); sort_col = PEER_COL_ADDRESS_COLLATED; break; case PEER_COL_CLIENT: r = gtk_cell_renderer_text_new(); c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL); break; case PEER_COL_PROGRESS: r = gtk_cell_renderer_progress_new(); c = gtk_tree_view_column_new_with_attributes(t, r, "value", PEER_COL_PROGRESS, NULL); break; case PEER_COL_ENCRYPTION_STOCK_ID: r = gtk_cell_renderer_pixbuf_new(); g_object_set(r, "xalign", (gfloat)0.0, "yalign", (gfloat)0.5, NULL); c = gtk_tree_view_column_new_with_attributes(t, r, "stock-id", PEER_COL_ENCRYPTION_STOCK_ID, NULL); gtk_tree_view_column_set_sizing(c, GTK_TREE_VIEW_COLUMN_FIXED); gtk_tree_view_column_set_fixed_width(c, 20); break; case PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING: r = gtk_cell_renderer_text_new(); c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL); sort_col = PEER_COL_DOWNLOAD_REQUEST_COUNT_INT; break; case PEER_COL_UPLOAD_REQUEST_COUNT_STRING: r = gtk_cell_renderer_text_new(); c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL); sort_col = PEER_COL_UPLOAD_REQUEST_COUNT_INT; break; case PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING: r = gtk_cell_renderer_text_new(); c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL); sort_col = PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT; break; case PEER_COL_BLOCKS_UPLOADED_COUNT_STRING: r = gtk_cell_renderer_text_new(); c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL); sort_col = PEER_COL_BLOCKS_UPLOADED_COUNT_INT; break; case PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING: r = gtk_cell_renderer_text_new(); c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL); sort_col = PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT; break; case PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING: r = gtk_cell_renderer_text_new(); c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL); sort_col = PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT; break; case PEER_COL_DOWNLOAD_RATE_STRING: r = gtk_cell_renderer_text_new(); g_object_set(G_OBJECT(r), "xalign", 1.0F, NULL); c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL); sort_col = PEER_COL_DOWNLOAD_RATE_DOUBLE; break; case PEER_COL_UPLOAD_RATE_STRING: r = gtk_cell_renderer_text_new(); g_object_set(G_OBJECT(r), "xalign", 1.0F, NULL); c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL); sort_col = PEER_COL_UPLOAD_RATE_DOUBLE; break; case PEER_COL_FLAGS: r = gtk_cell_renderer_text_new(); c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL); break; default: abort(); } gtk_tree_view_column_set_resizable(c, FALSE); gtk_tree_view_column_set_sort_column_id(c, sort_col); gtk_tree_view_append_column(GTK_TREE_VIEW(peer_view), c); } /* the 'expander' column has a 10-pixel margin on the left that doesn't look quite correct in any of these columns... so create a non-visible column and assign it as the 'expander column. */ { c = gtk_tree_view_column_new(); gtk_tree_view_column_set_visible(c, FALSE); gtk_tree_view_append_column(GTK_TREE_VIEW(peer_view), c); gtk_tree_view_set_expander_column(GTK_TREE_VIEW(peer_view), c); } } static void onMorePeerInfoToggled(GtkToggleButton* button, struct DetailsImpl* di) { tr_quark const key = TR_KEY_show_extra_peer_details; gboolean const value = gtk_toggle_button_get_active(button); gtr_core_set_pref_bool(di->core, key, value); setPeerViewColumns(GTK_TREE_VIEW(di->peer_view)); } static GtkWidget* peer_page_new(struct DetailsImpl* di) { gboolean b; char const* str; GtkListStore* store; GtkWidget* v; GtkWidget* w; GtkWidget* ret; GtkWidget* sw; GtkWidget* vbox; GtkWidget* webtree = NULL; GtkTreeModel* m; GtkTreeViewColumn* c; GtkCellRenderer* r; /* webseeds */ store = di->webseed_store = webseed_model_new(); v = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store)); g_signal_connect(v, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL); g_object_unref(store); str = getWebseedColumnNames(WEBSEED_COL_URL); r = gtk_cell_renderer_text_new(); g_object_set(G_OBJECT(r), "ellipsize", PANGO_ELLIPSIZE_END, NULL); c = gtk_tree_view_column_new_with_attributes(str, r, "text", WEBSEED_COL_URL, NULL); g_object_set(G_OBJECT(c), "expand", TRUE, NULL); gtk_tree_view_column_set_sort_column_id(c, WEBSEED_COL_URL); gtk_tree_view_append_column(GTK_TREE_VIEW(v), c); str = getWebseedColumnNames(WEBSEED_COL_DOWNLOAD_RATE_STRING); r = gtk_cell_renderer_text_new(); c = gtk_tree_view_column_new_with_attributes(str, r, "text", WEBSEED_COL_DOWNLOAD_RATE_STRING, NULL); gtk_tree_view_column_set_sort_column_id(c, WEBSEED_COL_DOWNLOAD_RATE_DOUBLE); gtk_tree_view_append_column(GTK_TREE_VIEW(v), c); w = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(w), GTK_SHADOW_IN); gtk_container_add(GTK_CONTAINER(w), v); webtree = w; di->webseed_view = w; /* peers */ store = di->peer_store = peer_store_new(); m = gtk_tree_model_sort_new_with_model(GTK_TREE_MODEL(store)); gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(m), PEER_COL_PROGRESS, GTK_SORT_DESCENDING); v = GTK_WIDGET(g_object_new(GTK_TYPE_TREE_VIEW, "model", m, "rules-hint", TRUE, "has-tooltip", TRUE, NULL)); di->peer_view = v; g_signal_connect(v, "query-tooltip", G_CALLBACK(onPeerViewQueryTooltip), di); g_object_unref(store); g_signal_connect(v, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL); setPeerViewColumns(GTK_TREE_VIEW(v)); w = sw = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(w), GTK_SHADOW_IN); gtk_container_add(GTK_CONTAINER(w), v); vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, GUI_PAD); gtk_container_set_border_width(GTK_CONTAINER(vbox), GUI_PAD_BIG); v = gtk_paned_new(GTK_ORIENTATION_VERTICAL); gtk_paned_pack1(GTK_PANED(v), webtree, FALSE, TRUE); gtk_paned_pack2(GTK_PANED(v), sw, TRUE, TRUE); gtk_box_pack_start(GTK_BOX(vbox), v, TRUE, TRUE, 0); w = gtk_check_button_new_with_mnemonic(_("Show _more details")); di->more_peer_details_check = w; b = gtr_pref_flag_get(TR_KEY_show_extra_peer_details); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), b); g_signal_connect(w, "toggled", G_CALLBACK(onMorePeerInfoToggled), di); gtk_box_pack_start(GTK_BOX(vbox), w, FALSE, FALSE, 0); /* ip-to-GtkTreeRowReference */ di->peer_hash = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)gtk_tree_row_reference_free); /* url-to-GtkTreeRowReference */ di->webseed_hash = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)gtk_tree_row_reference_free); ret = vbox; return ret; } /**** ***** ***** TRACKER ***** ****/ /* if it's been longer than a minute, don't bother showing the seconds */ static void tr_strltime_rounded(char* buf, time_t t, size_t buflen) { if (t > 60) { t -= (t % 60); } tr_strltime(buf, t, buflen); } static void buildTrackerSummary(GString* gstr, char const* key, tr_tracker_stat const* st, gboolean showScrape) { char* str; char timebuf[256]; time_t const now = time(NULL); char const* err_markup_begin = ""; char const* err_markup_end = ""; char const* timeout_markup_begin = ""; char const* timeout_markup_end = ""; char const* success_markup_begin = ""; char const* success_markup_end = ""; /* hostname */ { g_string_append(gstr, st->isBackup ? "" : ""); if (key != NULL) { str = g_markup_printf_escaped("%s - %s", st->host, key); } else { str = g_markup_printf_escaped("%s", st->host); } g_string_append(gstr, str); g_free(str); g_string_append(gstr, st->isBackup ? "" : ""); } if (!st->isBackup) { if (st->hasAnnounced && st->announceState != TR_TRACKER_INACTIVE) { g_string_append_c(gstr, '\n'); tr_strltime_rounded(timebuf, now - st->lastAnnounceTime, sizeof(timebuf)); if (st->lastAnnounceSucceeded) { g_string_append_printf(gstr, _("Got a list of %1$s%2$'d peers%3$s %4$s ago"), success_markup_begin, st->lastAnnouncePeerCount, success_markup_end, timebuf); } else if (st->lastAnnounceTimedOut) { g_string_append_printf(gstr, _("Peer list request %1$stimed out%2$s %3$s ago; will retry"), timeout_markup_begin, timeout_markup_end, timebuf); } else { g_string_append_printf(gstr, _("Got an error %1$s\"%2$s\"%3$s %4$s ago"), err_markup_begin, st->lastAnnounceResult, err_markup_end, timebuf); } } switch (st->announceState) { case TR_TRACKER_INACTIVE: g_string_append_c(gstr, '\n'); g_string_append(gstr, _("No updates scheduled")); break; case TR_TRACKER_WAITING: tr_strltime_rounded(timebuf, st->nextAnnounceTime - now, sizeof(timebuf)); g_string_append_c(gstr, '\n'); g_string_append_printf(gstr, _("Asking for more peers in %s"), timebuf); break; case TR_TRACKER_QUEUED: g_string_append_c(gstr, '\n'); g_string_append(gstr, _("Queued to ask for more peers")); break; case TR_TRACKER_ACTIVE: tr_strltime_rounded(timebuf, now - st->lastAnnounceStartTime, sizeof(timebuf)); g_string_append_c(gstr, '\n'); g_string_append_printf(gstr, _("Asking for more peers now… %s"), timebuf); break; } if (showScrape) { if (st->hasScraped) { g_string_append_c(gstr, '\n'); tr_strltime_rounded(timebuf, now - st->lastScrapeTime, sizeof(timebuf)); if (st->lastScrapeSucceeded) { g_string_append_printf(gstr, _("Tracker had %s%'d seeders and %'d leechers%s %s ago"), success_markup_begin, st->seederCount, st->leecherCount, success_markup_end, timebuf); } else { g_string_append_printf(gstr, _("Got a scrape error \"%s%s%s\" %s ago"), err_markup_begin, st->lastScrapeResult, err_markup_end, timebuf); } } switch (st->scrapeState) { case TR_TRACKER_INACTIVE: break; case TR_TRACKER_WAITING: g_string_append_c(gstr, '\n'); tr_strltime_rounded(timebuf, st->nextScrapeTime - now, sizeof(timebuf)); g_string_append_printf(gstr, _("Asking for peer counts in %s"), timebuf); break; case TR_TRACKER_QUEUED: g_string_append_c(gstr, '\n'); g_string_append(gstr, _("Queued to ask for peer counts")); break; case TR_TRACKER_ACTIVE: g_string_append_c(gstr, '\n'); tr_strltime_rounded(timebuf, now - st->lastScrapeStartTime, sizeof(timebuf)); g_string_append_printf(gstr, _("Asking for peer counts now… %s"), timebuf); break; } } } } enum { TRACKER_COL_TORRENT_ID, TRACKER_COL_TEXT, TRACKER_COL_IS_BACKUP, TRACKER_COL_TRACKER_ID, TRACKER_COL_FAVICON, TRACKER_COL_WAS_UPDATED, TRACKER_COL_KEY, TRACKER_N_COLS }; static gboolean trackerVisibleFunc(GtkTreeModel* model, GtkTreeIter* iter, gpointer data) { gboolean isBackup; struct DetailsImpl* di = data; /* show all */ if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(di->all_check))) { return TRUE; } /* don't show the backups... */ gtk_tree_model_get(model, iter, TRACKER_COL_IS_BACKUP, &isBackup, -1); return !isBackup; } static int tracker_list_get_current_torrent_id(struct DetailsImpl* di) { int torrent_id = -1; /* if there's only one torrent in the dialog, always use it */ if (g_slist_length(di->ids) == 1) { torrent_id = GPOINTER_TO_INT(di->ids->data); } /* otherwise, use the selected tracker's torrent */ if (torrent_id < 0) { GtkTreeIter iter; GtkTreeModel* model; GtkTreeSelection* sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(di->tracker_view)); if (gtk_tree_selection_get_selected(sel, &model, &iter)) { gtk_tree_model_get(model, &iter, TRACKER_COL_TORRENT_ID, &torrent_id, -1); } } return torrent_id; } static tr_torrent* tracker_list_get_current_torrent(struct DetailsImpl* di) { int const torrent_id = tracker_list_get_current_torrent_id(di); return gtr_core_find_torrent(di->core, torrent_id); } static void favicon_ready_cb(gpointer pixbuf, gpointer vreference) { GtkTreeIter iter; GtkTreeRowReference* reference = vreference; if (pixbuf != NULL) { GtkTreePath* path = gtk_tree_row_reference_get_path(reference); GtkTreeModel* model = gtk_tree_row_reference_get_model(reference); if (gtk_tree_model_get_iter(model, &iter, path)) { gtk_list_store_set(GTK_LIST_STORE(model), &iter, TRACKER_COL_FAVICON, pixbuf, -1); } gtk_tree_path_free(path); g_object_unref(pixbuf); } gtk_tree_row_reference_free(reference); } static void refreshTracker(struct DetailsImpl* di, tr_torrent** torrents, int n) { int* statCount; tr_tracker_stat** stats; GtkTreeIter iter; GtkTreeModel* model; GString* gstr = di->gstr; /* buffer for temporary strings */ GHashTable* hash = di->tracker_hash; GtkListStore* store = di->tracker_store; tr_session* session = gtr_core_session(di->core); gboolean const showScrape = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(di->scrape_check)); /* step 1: get all the trackers */ statCount = g_new0(int, n); stats = g_new0(tr_tracker_stat*, n); for (int i = 0; i < n; ++i) { stats[i] = tr_torrentTrackers(torrents[i], &statCount[i]); } /* step 2: mark all the trackers in the list as not-updated */ model = GTK_TREE_MODEL(store); if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0)) { do { gtk_list_store_set(store, &iter, TRACKER_COL_WAS_UPDATED, FALSE, -1); } while (gtk_tree_model_iter_next(model, &iter)); } /* step 3: add any new trackers */ for (int i = 0; i < n; ++i) { int const jn = statCount[i]; for (int j = 0; j < jn; ++j) { tr_torrent const* tor = torrents[i]; tr_tracker_stat const* st = &stats[i][j]; int const torrent_id = tr_torrentId(tor); /* build the key to find the row */ g_string_truncate(gstr, 0); g_string_append_printf(gstr, "%d\t%d\t%s", torrent_id, st->tier, st->announce); if (g_hash_table_lookup(hash, gstr->str) == NULL) { GtkTreePath* p; GtkTreeRowReference* ref; gtk_list_store_insert_with_values(store, &iter, -1, TRACKER_COL_TORRENT_ID, torrent_id, TRACKER_COL_TRACKER_ID, st->id, TRACKER_COL_KEY, gstr->str, -1); p = gtk_tree_model_get_path(model, &iter); ref = gtk_tree_row_reference_new(model, p); g_hash_table_insert(hash, g_strdup(gstr->str), ref); ref = gtk_tree_row_reference_new(model, p); gtr_get_favicon_from_url(session, st->announce, favicon_ready_cb, ref); gtk_tree_path_free(p); } } } /* step 4: update the peers */ for (int i = 0; i < n; ++i) { tr_torrent const* tor = torrents[i]; char const* summary_name = n > 1 ? tr_torrentName(tor) : NULL; for (int j = 0; j < statCount[i]; ++j) { GtkTreePath* p; GtkTreeRowReference* ref; tr_tracker_stat const* st = &stats[i][j]; /* build the key to find the row */ g_string_truncate(gstr, 0); g_string_append_printf(gstr, "%d\t%d\t%s", tr_torrentId(tor), st->tier, st->announce); ref = g_hash_table_lookup(hash, gstr->str); p = gtk_tree_row_reference_get_path(ref); gtk_tree_model_get_iter(model, &iter, p); /* update the row */ g_string_truncate(gstr, 0); buildTrackerSummary(gstr, summary_name, st, showScrape); gtk_list_store_set(store, &iter, TRACKER_COL_TEXT, gstr->str, TRACKER_COL_IS_BACKUP, st->isBackup, TRACKER_COL_TRACKER_ID, st->id, TRACKER_COL_WAS_UPDATED, TRUE, -1); /* cleanup */ gtk_tree_path_free(p); } } /* step 5: remove trackers that have disappeared */ if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0)) { gboolean more = TRUE; while (more) { gboolean b; gtk_tree_model_get(model, &iter, TRACKER_COL_WAS_UPDATED, &b, -1); if (b) { more = gtk_tree_model_iter_next(model, &iter); } else { char* key; gtk_tree_model_get(model, &iter, TRACKER_COL_KEY, &key, -1); g_hash_table_remove(hash, key); more = gtk_list_store_remove(store, &iter); g_free(key); } } } gtk_widget_set_sensitive(di->edit_trackers_button, tracker_list_get_current_torrent_id(di) >= 0); /* cleanup */ for (int i = 0; i < n; ++i) { tr_torrentTrackersFree(stats[i], statCount[i]); } g_free(stats); g_free(statCount); } static void onScrapeToggled(GtkToggleButton* button, struct DetailsImpl* di) { tr_quark const key = TR_KEY_show_tracker_scrapes; gboolean const value = gtk_toggle_button_get_active(button); gtr_core_set_pref_bool(di->core, key, value); refresh(di); } static void onBackupToggled(GtkToggleButton* button, struct DetailsImpl* di) { tr_quark const key = TR_KEY_show_backup_trackers; gboolean const value = gtk_toggle_button_get_active(button); gtr_core_set_pref_bool(di->core, key, value); refresh(di); } static void on_edit_trackers_response(GtkDialog* dialog, int response, gpointer data) { gboolean do_destroy = TRUE; struct DetailsImpl* di = data; if (response == GTK_RESPONSE_ACCEPT) { int n; int tier; GtkTextIter start; GtkTextIter end; int const torrent_id = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(dialog), TORRENT_ID_KEY)); GtkTextBuffer* text_buffer = g_object_get_qdata(G_OBJECT(dialog), TEXT_BUFFER_KEY); tr_torrent* tor = gtr_core_find_torrent(di->core, torrent_id); if (tor != NULL) { tr_tracker_info* trackers; char** tracker_strings; char* tracker_text; /* build the array of trackers */ gtk_text_buffer_get_bounds(text_buffer, &start, &end); tracker_text = gtk_text_buffer_get_text(text_buffer, &start, &end, FALSE); tracker_strings = g_strsplit(tracker_text, "\n", 0); trackers = g_new0(tr_tracker_info, g_strv_length(tracker_strings)); n = 0; tier = 0; for (int i = 0; tracker_strings[i] != NULL; ++i) { char* const str = tracker_strings[i]; if (tr_str_is_empty(str)) { ++tier; } else { trackers[n].tier = tier; trackers[n].announce = str; ++n; } } /* update the torrent */ if (tr_torrentSetAnnounceList(tor, trackers, n)) { refresh(di); } else { GtkWidget* w; char const* text = _("List contains invalid URLs"); w = gtk_message_dialog_new(GTK_WINDOW(dialog), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", text); gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(w), "%s", _("Please correct the errors and try again.")); gtk_dialog_run(GTK_DIALOG(w)); gtk_widget_destroy(w); do_destroy = FALSE; } /* cleanup */ g_free(trackers); g_strfreev(tracker_strings); g_free(tracker_text); } } if (do_destroy) { gtk_widget_destroy(GTK_WIDGET(dialog)); } } static void get_editable_tracker_list(GString* gstr, tr_torrent const* tor) { int tier = 0; tr_info const* inf = tr_torrentInfo(tor); for (unsigned int i = 0; i < inf->trackerCount; ++i) { tr_tracker_info const* t = &inf->trackers[i]; if (tier != t->tier) { tier = t->tier; g_string_append_c(gstr, '\n'); } g_string_append_printf(gstr, "%s\n", t->announce); } if (gstr->len > 0) { g_string_truncate(gstr, gstr->len - 1); } } static void on_edit_trackers(GtkButton* button, gpointer data) { struct DetailsImpl* di = data; tr_torrent* tor = tracker_list_get_current_torrent(di); if (tor != NULL) { guint row; GtkWidget* w; GtkWidget* d; GtkWidget* fr; GtkWidget* t; GtkWidget* l; GtkWidget* sw; GtkWindow* win = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button))); GString* gstr = di->gstr; /* buffer for temporary strings */ int const torrent_id = tr_torrentId(tor); g_string_truncate(gstr, 0); g_string_append_printf(gstr, _("%s - Edit Trackers"), tr_torrentName(tor)); d = gtk_dialog_new_with_buttons(gstr->str, win, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, _("_Cancel"), GTK_RESPONSE_CANCEL, _("_Save"), GTK_RESPONSE_ACCEPT, NULL); g_signal_connect(d, "response", G_CALLBACK(on_edit_trackers_response), data); row = 0; t = hig_workarea_create(); hig_workarea_add_section_title(t, &row, _("Tracker Announce URLs")); l = gtk_label_new(NULL); gtk_label_set_markup(GTK_LABEL(l), _("To add a backup URL, add it on the line after the primary URL.\n" "To add another primary URL, add it after a blank line.")); gtk_label_set_justify(GTK_LABEL(l), GTK_JUSTIFY_LEFT); g_object_set(l, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_CENTER, NULL); hig_workarea_add_wide_control(t, &row, l); w = gtk_text_view_new(); g_string_truncate(gstr, 0); get_editable_tracker_list(gstr, tor); gtk_text_buffer_set_text(gtk_text_view_get_buffer(GTK_TEXT_VIEW(w)), gstr->str, -1); fr = gtk_frame_new(NULL); gtk_frame_set_shadow_type(GTK_FRAME(fr), GTK_SHADOW_IN); sw = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_container_add(GTK_CONTAINER(sw), w); gtk_container_add(GTK_CONTAINER(fr), sw); gtk_widget_set_size_request(fr, 500U, 166U); hig_workarea_add_wide_tall_control(t, &row, fr); gtr_dialog_set_content(GTK_DIALOG(d), t); g_object_set_qdata(G_OBJECT(d), TORRENT_ID_KEY, GINT_TO_POINTER(torrent_id)); g_object_set_qdata(G_OBJECT(d), TEXT_BUFFER_KEY, gtk_text_view_get_buffer(GTK_TEXT_VIEW(w))); gtk_widget_show(d); } } static void on_tracker_list_selection_changed(GtkTreeSelection* sel, gpointer gdi) { struct DetailsImpl* di = gdi; int const n = gtk_tree_selection_count_selected_rows(sel); tr_torrent* tor = tracker_list_get_current_torrent(di); gtk_widget_set_sensitive(di->remove_tracker_button, n > 0); gtk_widget_set_sensitive(di->add_tracker_button, tor != NULL); gtk_widget_set_sensitive(di->edit_trackers_button, tor != NULL); } static void on_add_tracker_response(GtkDialog* dialog, int response, gpointer gdi) { gboolean destroy = TRUE; if (response == GTK_RESPONSE_ACCEPT) { struct DetailsImpl* di = gdi; GtkWidget* e = GTK_WIDGET(g_object_get_qdata(G_OBJECT(dialog), URL_ENTRY_KEY)); int const torrent_id = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(dialog), TORRENT_ID_KEY)); char* url = g_strdup(gtk_entry_get_text(GTK_ENTRY(e))); g_strstrip(url); if (!tr_str_is_empty(url)) { if (tr_urlIsValidTracker(url)) { tr_variant top; tr_variant* args; tr_variant* trackers; tr_variantInitDict(&top, 2); tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set"); args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2); tr_variantDictAddInt(args, TR_KEY_id, torrent_id); trackers = tr_variantDictAddList(args, TR_KEY_trackerAdd, 1); tr_variantListAddStr(trackers, url); gtr_core_exec(di->core, &top); refresh(di); tr_variantFree(&top); } else { gtr_unrecognized_url_dialog(GTK_WIDGET(dialog), url); destroy = FALSE; } } g_free(url); } if (destroy) { gtk_widget_destroy(GTK_WIDGET(dialog)); } } static void on_tracker_list_add_button_clicked(GtkButton* button, gpointer gdi) { TR_UNUSED(button); struct DetailsImpl* di = gdi; tr_torrent* tor = tracker_list_get_current_torrent(di); if (tor != NULL) { guint row; GtkWidget* e; GtkWidget* t; GtkWidget* w; GString* gstr = di->gstr; /* buffer for temporary strings */ g_string_truncate(gstr, 0); g_string_append_printf(gstr, _("%s - Add Tracker"), tr_torrentName(tor)); w = gtk_dialog_new_with_buttons(gstr->str, GTK_WINDOW(di->dialog), GTK_DIALOG_DESTROY_WITH_PARENT, _("_Cancel"), GTK_RESPONSE_CANCEL, _("_Add"), GTK_RESPONSE_ACCEPT, NULL); g_signal_connect(w, "response", G_CALLBACK(on_add_tracker_response), gdi); row = 0; t = hig_workarea_create(); hig_workarea_add_section_title(t, &row, _("Tracker")); e = gtk_entry_new(); gtk_widget_set_size_request(e, 400, -1); gtr_paste_clipboard_url_into_entry(e); g_object_set_qdata(G_OBJECT(w), URL_ENTRY_KEY, e); g_object_set_qdata(G_OBJECT(w), TORRENT_ID_KEY, GINT_TO_POINTER(tr_torrentId(tor))); hig_workarea_add_row(t, &row, _("_Announce URL:"), e, NULL); gtr_dialog_set_content(GTK_DIALOG(w), t); gtk_widget_show_all(w); } } static void on_tracker_list_remove_button_clicked(GtkButton* button, gpointer gdi) { TR_UNUSED(button); GtkTreeIter iter; GtkTreeModel* model; struct DetailsImpl* di = gdi; GtkTreeView* v = GTK_TREE_VIEW(di->tracker_view); GtkTreeSelection* sel = gtk_tree_view_get_selection(v); if (gtk_tree_selection_get_selected(sel, &model, &iter)) { int torrent_id; int tracker_id; tr_variant top; tr_variant* args; tr_variant* trackers; gtk_tree_model_get(model, &iter, TRACKER_COL_TRACKER_ID, &tracker_id, TRACKER_COL_TORRENT_ID, &torrent_id, -1); tr_variantInitDict(&top, 2); tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set"); args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2); tr_variantDictAddInt(args, TR_KEY_id, torrent_id); trackers = tr_variantDictAddList(args, TR_KEY_trackerRemove, 1); tr_variantListAddInt(trackers, tracker_id); gtr_core_exec(di->core, &top); refresh(di); tr_variantFree(&top); } } static GtkWidget* tracker_page_new(struct DetailsImpl* di) { gboolean b; GtkCellRenderer* r; GtkTreeViewColumn* c; GtkTreeSelection* sel; GtkWidget* vbox; GtkWidget* sw; GtkWidget* w; GtkWidget* v; GtkWidget* hbox; int const pad = (GUI_PAD + GUI_PAD_BIG) / 2; vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, GUI_PAD); gtk_container_set_border_width(GTK_CONTAINER(vbox), GUI_PAD_BIG); di->tracker_store = gtk_list_store_new(TRACKER_N_COLS, G_TYPE_INT, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_INT, GDK_TYPE_PIXBUF, G_TYPE_BOOLEAN, G_TYPE_STRING); di->tracker_hash = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)gtk_tree_row_reference_free); di->trackers_filtered = gtk_tree_model_filter_new(GTK_TREE_MODEL(di->tracker_store), NULL); gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(di->trackers_filtered), trackerVisibleFunc, di, NULL); hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD_BIG); v = di->tracker_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(di->trackers_filtered)); g_object_unref(di->trackers_filtered); gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(v), FALSE); g_signal_connect(v, "button-press-event", G_CALLBACK(on_tree_view_button_pressed), NULL); g_signal_connect(v, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL); sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(v)); g_signal_connect(sel, "changed", G_CALLBACK(on_tracker_list_selection_changed), di); c = gtk_tree_view_column_new(); gtk_tree_view_column_set_title(c, _("Trackers")); gtk_tree_view_append_column(GTK_TREE_VIEW(v), c); r = gtk_cell_renderer_pixbuf_new(); g_object_set(r, "width", 20 + (GUI_PAD_SMALL * 2), "xpad", GUI_PAD_SMALL, "ypad", pad, "yalign", 0.0F, NULL); gtk_tree_view_column_pack_start(c, r, FALSE); gtk_tree_view_column_add_attribute(c, r, "pixbuf", TRACKER_COL_FAVICON); r = gtk_cell_renderer_text_new(); g_object_set(G_OBJECT(r), "ellipsize", PANGO_ELLIPSIZE_END, "xpad", GUI_PAD_SMALL, "ypad", pad, NULL); gtk_tree_view_column_pack_start(c, r, TRUE); gtk_tree_view_column_add_attribute(c, r, "markup", TRACKER_COL_TEXT); sw = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_container_add(GTK_CONTAINER(sw), v); w = gtk_frame_new(NULL); gtk_frame_set_shadow_type(GTK_FRAME(w), GTK_SHADOW_IN); gtk_container_add(GTK_CONTAINER(w), sw); gtk_box_pack_start(GTK_BOX(hbox), w, TRUE, TRUE, 0); v = gtk_box_new(GTK_ORIENTATION_VERTICAL, GUI_PAD); w = gtk_button_new_with_mnemonic(_("_Add")); di->add_tracker_button = w; g_signal_connect(w, "clicked", G_CALLBACK(on_tracker_list_add_button_clicked), di); gtk_box_pack_start(GTK_BOX(v), w, FALSE, FALSE, 0); w = gtk_button_new_with_mnemonic(_("_Edit")); g_signal_connect(w, "clicked", G_CALLBACK(on_edit_trackers), di); di->edit_trackers_button = w; gtk_box_pack_start(GTK_BOX(v), w, FALSE, FALSE, 0); w = gtk_button_new_with_mnemonic(_("_Remove")); di->remove_tracker_button = w; g_signal_connect(w, "clicked", G_CALLBACK(on_tracker_list_remove_button_clicked), di); gtk_box_pack_start(GTK_BOX(v), w, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), v, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0); w = gtk_check_button_new_with_mnemonic(_("Show _more details")); di->scrape_check = w; b = gtr_pref_flag_get(TR_KEY_show_tracker_scrapes); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), b); g_signal_connect(w, "toggled", G_CALLBACK(onScrapeToggled), di); gtk_box_pack_start(GTK_BOX(vbox), w, FALSE, FALSE, 0); w = gtk_check_button_new_with_mnemonic(_("Show _backup trackers")); di->all_check = w; b = gtr_pref_flag_get(TR_KEY_show_backup_trackers); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), b); g_signal_connect(w, "toggled", G_CALLBACK(onBackupToggled), di); gtk_box_pack_start(GTK_BOX(vbox), w, FALSE, FALSE, 0); return vbox; } /**** ***** DIALOG ****/ static void refresh(struct DetailsImpl* di) { int n; tr_torrent** torrents = getTorrents(di, &n); refreshInfo(di, torrents, n); refreshPeers(di, torrents, n); refreshTracker(di, torrents, n); refreshOptions(di, torrents, n); if (n == 0) { gtk_dialog_response(GTK_DIALOG(di->dialog), GTK_RESPONSE_CLOSE); } g_free(torrents); } static gboolean periodic_refresh(gpointer data) { refresh(data); return G_SOURCE_CONTINUE; } static void on_details_window_size_allocated(GtkWidget* gtk_window, GtkAllocation* alloc, gpointer gdata) { TR_UNUSED(alloc); TR_UNUSED(gdata); int w = 0; int h = 0; gtk_window_get_size(GTK_WINDOW(gtk_window), &w, &h); gtr_pref_int_set(TR_KEY_details_window_width, w); gtr_pref_int_set(TR_KEY_details_window_height, h); } static void details_free(gpointer gdata) { struct DetailsImpl* data = gdata; g_source_remove(data->periodic_refresh_tag); g_hash_table_destroy(data->tracker_hash); g_hash_table_destroy(data->webseed_hash); g_hash_table_destroy(data->peer_hash); g_string_free(data->gstr, TRUE); g_slist_free(data->ids); g_free(data); } GtkWidget* gtr_torrent_details_dialog_new(GtkWindow* parent, TrCore* core) { GtkWidget* d; GtkWidget* n; GtkWidget* v; GtkWidget* w; GtkWidget* l; struct DetailsImpl* di = g_new0(struct DetailsImpl, 1); /* one-time setup */ if (ARG_KEY == 0) { ARG_KEY = g_quark_from_static_string("tr-arg-key"); DETAILS_KEY = g_quark_from_static_string("tr-details-data-key"); TORRENT_ID_KEY = g_quark_from_static_string("tr-torrent-id-key"); TEXT_BUFFER_KEY = g_quark_from_static_string("tr-text-buffer-key"); URL_ENTRY_KEY = g_quark_from_static_string("tr-url-entry-key"); } /* create the dialog */ di->core = core; di->gstr = g_string_new(NULL); d = gtk_dialog_new_with_buttons(NULL, parent, 0, _("_Close"), GTK_RESPONSE_CLOSE, NULL); di->dialog = d; gtk_window_set_role(GTK_WINDOW(d), "tr-info"); /* return saved window size */ gtk_window_resize(GTK_WINDOW(d), gtr_pref_int_get(TR_KEY_details_window_width), gtr_pref_int_get(TR_KEY_details_window_height)); g_signal_connect(d, "size-allocate", G_CALLBACK(on_details_window_size_allocated), NULL); g_signal_connect_swapped(d, "response", G_CALLBACK(gtk_widget_destroy), d); gtk_container_set_border_width(GTK_CONTAINER(d), GUI_PAD); g_object_set_qdata_full(G_OBJECT(d), DETAILS_KEY, di, details_free); n = gtk_notebook_new(); gtk_container_set_border_width(GTK_CONTAINER(n), GUI_PAD); w = info_page_new(di); l = gtk_label_new(_("Information")); gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l); w = peer_page_new(di); l = gtk_label_new(_("Peers")); gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l); w = tracker_page_new(di); l = gtk_label_new(_("Trackers")); gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l); v = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); di->file_list = gtr_file_list_new(core, 0); di->file_label = gtk_label_new(_("File listing not available for combined torrent properties")); gtk_box_pack_start(GTK_BOX(v), di->file_list, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(v), di->file_label, TRUE, TRUE, 0); gtk_container_set_border_width(GTK_CONTAINER(v), GUI_PAD_BIG); l = gtk_label_new(_("Files")); gtk_notebook_append_page(GTK_NOTEBOOK(n), v, l); w = options_page_new(di); l = gtk_label_new(_("Options")); gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l); gtr_dialog_set_content(GTK_DIALOG(d), n); di->periodic_refresh_tag = gdk_threads_add_timeout_seconds(SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS, periodic_refresh, di); return d; } void gtr_torrent_details_dialog_set_torrents(GtkWidget* w, GSList* ids) { char title[256]; int const len = g_slist_length(ids); struct DetailsImpl* di = g_object_get_qdata(G_OBJECT(w), DETAILS_KEY); g_slist_free(di->ids); di->ids = g_slist_copy(ids); if (len == 1) { int const id = GPOINTER_TO_INT(ids->data); tr_torrent* tor = gtr_core_find_torrent(di->core, id); tr_info const* inf = tr_torrentInfo(tor); g_snprintf(title, sizeof(title), _("%s Properties"), inf->name); gtr_file_list_set_torrent(di->file_list, id); gtk_widget_show(di->file_list); gtk_widget_hide(di->file_label); } else { gtr_file_list_clear(di->file_list); gtk_widget_hide(di->file_list); gtk_widget_show(di->file_label); g_snprintf(title, sizeof(title), _("%'d Torrent Properties"), len); } gtk_window_set_title(GTK_WINDOW(w), title); refresh(di); }