mirror of
https://github.com/transmission/transmission
synced 2024-12-27 18:18:10 +00:00
2595 lines
92 KiB
C
2595 lines
92 KiB
C
/*
|
|
* This file Copyright (C) Mnemosyne LLC
|
|
*
|
|
* This file is licensed by the GPL version 2. Works owned by the
|
|
* Transmission project are granted a special exemption to clause 2 (b)
|
|
* so that the bulk of its code can remain under the MIT license.
|
|
* This exemption does not extend to derived works not owned by
|
|
* the Transmission project.
|
|
*
|
|
* $Id$
|
|
*/
|
|
|
|
#include <stddef.h>
|
|
#include <stdio.h> /* sscanf () */
|
|
#include <stdlib.h> /* abort () */
|
|
#include <glib/gi18n.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <libtransmission/transmission.h>
|
|
#include <libtransmission/utils.h> /* 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)
|
|
{
|
|
GSList * l;
|
|
int torrentCount = 0;
|
|
const int n = g_slist_length (d->ids);
|
|
tr_torrent ** torrents = g_new (tr_torrent*, n);
|
|
|
|
for (l=d->ids; l!=NULL; l=l->next)
|
|
if ((torrents[torrentCount] = gtr_core_find_torrent (d->core, GPOINTER_TO_INT (l->data))))
|
|
++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);
|
|
const gboolean 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);
|
|
const int 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);
|
|
const double 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) {
|
|
const bool baseline = tr_torrentUsesSessionLimits (torrents[0]);
|
|
int i;
|
|
for (i=1; i<n; ++i)
|
|
if (baseline != tr_torrentUsesSessionLimits (torrents[i]))
|
|
break;
|
|
if (i == n)
|
|
set_togglebutton_if_different (di->honor_limits_check,
|
|
di->honor_limits_check_tag, baseline);
|
|
}
|
|
|
|
/* down_limited_check */
|
|
if (n) {
|
|
const bool baseline = tr_torrentUsesSpeedLimit (torrents[0], TR_DOWN);
|
|
int i;
|
|
for (i=1; i<n; ++i)
|
|
if (baseline != tr_torrentUsesSpeedLimit (torrents[i], TR_DOWN))
|
|
break;
|
|
if (i == n)
|
|
set_togglebutton_if_different (di->down_limited_check,
|
|
di->down_limited_check_tag, baseline);
|
|
}
|
|
|
|
/* down_limit_spin */
|
|
if (n) {
|
|
const unsigned int baseline = tr_torrentGetSpeedLimit_KBps (torrents[0], TR_DOWN);
|
|
int i;
|
|
for (i=1; i<n; ++i)
|
|
if (baseline != (tr_torrentGetSpeedLimit_KBps (torrents[i], TR_DOWN)))
|
|
break;
|
|
if (i == n)
|
|
set_int_spin_if_different (di->down_limit_spin,
|
|
di->down_limit_spin_tag, baseline);
|
|
}
|
|
|
|
/* up_limited_check */
|
|
if (n) {
|
|
const bool baseline = tr_torrentUsesSpeedLimit (torrents[0], TR_UP);
|
|
int i;
|
|
for (i=1; i<n; ++i)
|
|
if (baseline != tr_torrentUsesSpeedLimit (torrents[i], TR_UP))
|
|
break;
|
|
if (i == n)
|
|
set_togglebutton_if_different (di->up_limited_check,
|
|
di->up_limited_check_tag, baseline);
|
|
}
|
|
|
|
/* up_limit_sping */
|
|
if (n) {
|
|
const unsigned int baseline = tr_torrentGetSpeedLimit_KBps (torrents[0], TR_UP);
|
|
int i;
|
|
for (i=1; i<n; ++i)
|
|
if (baseline != (tr_torrentGetSpeedLimit_KBps (torrents[i], TR_UP)))
|
|
break;
|
|
if (i == n)
|
|
set_int_spin_if_different (di->up_limit_sping,
|
|
di->up_limit_spin_tag, baseline);
|
|
}
|
|
|
|
/* bandwidth_combo */
|
|
if (n) {
|
|
const int baseline = tr_torrentGetPriority (torrents[0]);
|
|
int i;
|
|
for (i=1; i<n; ++i)
|
|
if (baseline != tr_torrentGetPriority (torrents[i]))
|
|
break;
|
|
if (i == n) {
|
|
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 */
|
|
/* ratio_spin */
|
|
if (n) {
|
|
int i;
|
|
const int baseline = tr_torrentGetRatioMode (torrents[0]);
|
|
for (i=1; i<n; ++i)
|
|
if (baseline != (int)tr_torrentGetRatioMode (torrents[i]))
|
|
break;
|
|
if (i == n) {
|
|
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);
|
|
}
|
|
}
|
|
if (n) {
|
|
const double baseline = tr_torrentGetRatioLimit (torrents[0]);
|
|
set_double_spin_if_different (di->ratio_spin,
|
|
di->ratio_spin_tag, baseline);
|
|
}
|
|
|
|
/* idle_combo */
|
|
/* idle_spin */
|
|
if (n) {
|
|
int i;
|
|
const int baseline = tr_torrentGetIdleMode (torrents[0]);
|
|
for (i=1; i<n; ++i)
|
|
if (baseline != (int)tr_torrentGetIdleMode (torrents[i]))
|
|
break;
|
|
if (i == n) {
|
|
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);
|
|
}
|
|
}
|
|
if (n) {
|
|
const int baseline = tr_torrentGetIdleLimit (torrents[0]);
|
|
set_int_spin_if_different (di->idle_spin,
|
|
di->idle_spin_tag, baseline);
|
|
}
|
|
|
|
/* max_peers_spin */
|
|
if (n) {
|
|
const int 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, const char * key, gboolean value)
|
|
{
|
|
GSList *l;
|
|
tr_benc top, *args, *ids;
|
|
|
|
tr_bencInitDict (&top, 2);
|
|
tr_bencDictAddStr (&top, "method", "torrent-set");
|
|
args = tr_bencDictAddDict (&top, "arguments", 2);
|
|
tr_bencDictAddBool (args, key, value);
|
|
ids = tr_bencDictAddList (args, "ids", g_slist_length (di->ids));
|
|
for (l=di->ids; l; l=l->next)
|
|
tr_bencListAddInt (ids, GPOINTER_TO_INT (l->data));
|
|
|
|
gtr_core_exec (di->core, &top);
|
|
tr_bencFree (&top);
|
|
}
|
|
|
|
static void
|
|
torrent_set_int (struct DetailsImpl * di, const char * key, int value)
|
|
{
|
|
GSList *l;
|
|
tr_benc top, *args, *ids;
|
|
|
|
tr_bencInitDict (&top, 2);
|
|
tr_bencDictAddStr (&top, "method", "torrent-set");
|
|
args = tr_bencDictAddDict (&top, "arguments", 2);
|
|
tr_bencDictAddInt (args, key, value);
|
|
ids = tr_bencDictAddList (args, "ids", g_slist_length (di->ids));
|
|
for (l=di->ids; l; l=l->next)
|
|
tr_bencListAddInt (ids, GPOINTER_TO_INT (l->data));
|
|
|
|
gtr_core_exec (di->core, &top);
|
|
tr_bencFree (&top);
|
|
}
|
|
|
|
static void
|
|
torrent_set_real (struct DetailsImpl * di, const char * key, double value)
|
|
{
|
|
GSList *l;
|
|
tr_benc top, *args, *ids;
|
|
|
|
tr_bencInitDict (&top, 2);
|
|
tr_bencDictAddStr (&top, "method", "torrent-set");
|
|
args = tr_bencDictAddDict (&top, "arguments", 2);
|
|
tr_bencDictAddReal (args, key, value);
|
|
ids = tr_bencDictAddList (args, "ids", g_slist_length (di->ids));
|
|
for (l=di->ids; l; l=l->next)
|
|
tr_bencListAddInt (ids, GPOINTER_TO_INT (l->data));
|
|
|
|
gtr_core_exec (di->core, &top);
|
|
tr_bencFree (&top);
|
|
}
|
|
|
|
static void
|
|
up_speed_toggled_cb (GtkToggleButton * tb, gpointer d)
|
|
{
|
|
torrent_set_bool (d, "uploadLimited", gtk_toggle_button_get_active (tb));
|
|
}
|
|
|
|
static void
|
|
down_speed_toggled_cb (GtkToggleButton *tb, gpointer d)
|
|
{
|
|
torrent_set_bool (d, "downloadLimited", gtk_toggle_button_get_active (tb));
|
|
}
|
|
|
|
static void
|
|
global_speed_toggled_cb (GtkToggleButton * tb, gpointer d)
|
|
{
|
|
torrent_set_bool (d, "honorsSessionLimits", gtk_toggle_button_get_active (tb));
|
|
}
|
|
|
|
static void
|
|
up_speed_spun_cb (GtkSpinButton * s, struct DetailsImpl * di)
|
|
{
|
|
torrent_set_int (di, "uploadLimit", gtk_spin_button_get_value_as_int (s));
|
|
}
|
|
|
|
static void
|
|
down_speed_spun_cb (GtkSpinButton * s, struct DetailsImpl * di)
|
|
{
|
|
torrent_set_int (di, "downloadLimit", gtk_spin_button_get_value_as_int (s));
|
|
}
|
|
|
|
static void
|
|
idle_spun_cb (GtkSpinButton * s, struct DetailsImpl * di)
|
|
{
|
|
torrent_set_int (di, "seedIdleLimit", gtk_spin_button_get_value_as_int (s));
|
|
}
|
|
|
|
static void
|
|
ratio_spun_cb (GtkSpinButton * s, struct DetailsImpl * di)
|
|
{
|
|
torrent_set_real (di, "seedRatioLimit", gtk_spin_button_get_value (s));
|
|
}
|
|
|
|
static void
|
|
max_peers_spun_cb (GtkSpinButton * s, struct DetailsImpl * di)
|
|
{
|
|
torrent_set_int (di, "peer-limit", gtk_spin_button_get_value (s));
|
|
}
|
|
|
|
static void
|
|
onPriorityChanged (GtkComboBox * combo_box, struct DetailsImpl * di)
|
|
{
|
|
const tr_priority_t priority = gtr_priority_combo_get_value (combo_box);
|
|
torrent_set_int (di, "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)
|
|
{
|
|
const char * key = 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, (gpointer)"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, (gpointer)"seedIdleMode");
|
|
return w;
|
|
}
|
|
|
|
static GtkWidget*
|
|
options_page_new (struct DetailsImpl * d)
|
|
{
|
|
guint row;
|
|
gulong tag;
|
|
char buf[128];
|
|
GtkWidget *t, *w, *tb, *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, INT_MAX, 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 const char *
|
|
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, const char * str)
|
|
{
|
|
char * old_str;
|
|
GtkTextIter start, 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) || strcmp (old_str, str))
|
|
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)
|
|
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)
|
|
{
|
|
int i;
|
|
const char * str;
|
|
const char * mixed = _("Mixed");
|
|
const char * no_torrent = _("No Torrents Selected");
|
|
const char * stateString;
|
|
char buf[512];
|
|
uint64_t sizeWhenDone = 0;
|
|
const tr_stat ** stats = g_new (const tr_stat*, n);
|
|
const tr_info ** infos = g_new (const tr_info*, n);
|
|
for (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 {
|
|
const bool baseline = infos[0]->isPrivate;
|
|
for (i=1; i<n; ++i)
|
|
if (baseline != infos[i]->isPrivate)
|
|
break;
|
|
if (i!=n)
|
|
str = mixed;
|
|
else if (baseline)
|
|
str = _("Private to this tracker -- DHT and PEX disabled");
|
|
else
|
|
str = _("Public torrent");
|
|
}
|
|
gtr_label_set_text (GTK_LABEL (di->privacy_lb), str);
|
|
|
|
|
|
/* origin_lb */
|
|
if (n<=0)
|
|
str = no_torrent;
|
|
else {
|
|
const char * creator = infos[0]->creator ? infos[0]->creator : "";
|
|
const time_t date = infos[0]->dateCreated;
|
|
char * datestr = get_short_date_string (date);
|
|
gboolean mixed_creator = FALSE;
|
|
gboolean mixed_date = FALSE;
|
|
|
|
for (i=1; i<n; ++i) {
|
|
mixed_creator |= strcmp (creator, infos[i]->creator ? infos[i]->creator : "");
|
|
mixed_date |= (date != infos[i]->dateCreated);
|
|
}
|
|
if (mixed_date && mixed_creator)
|
|
str = mixed;
|
|
else {
|
|
if (mixed_date)
|
|
g_snprintf (buf, sizeof (buf), _("Created by %1$s"), creator);
|
|
else if (mixed_creator || !*creator)
|
|
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 {
|
|
const char * baseline = infos[0]->comment ? infos[0]->comment : "";
|
|
for (i=1; i<n; ++i)
|
|
if (strcmp (baseline, infos[i]->comment ? infos[i]->comment : ""))
|
|
break;
|
|
if (i==n)
|
|
str = baseline;
|
|
else
|
|
str = mixed;
|
|
}
|
|
gtr_text_buffer_set_text (di->comment_buffer, str);
|
|
|
|
/* destination_lb */
|
|
if (n<=0)
|
|
str = no_torrent;
|
|
else {
|
|
const char * baseline = tr_torrentGetDownloadDir (torrents[0]);
|
|
for (i=1; i<n; ++i)
|
|
if (strcmp (baseline, tr_torrentGetDownloadDir (torrents[i])))
|
|
break;
|
|
if (i==n)
|
|
str = baseline;
|
|
else
|
|
str = mixed;
|
|
}
|
|
gtr_label_set_text (GTK_LABEL (di->destination_lb), str);
|
|
|
|
/* state_lb */
|
|
if (n<=0)
|
|
str = no_torrent;
|
|
else {
|
|
const tr_torrent_activity activity = stats[0]->activity;
|
|
bool allFinished = stats[0]->finished;
|
|
for (i=1; i<n; ++i) {
|
|
if (activity != stats[i]->activity)
|
|
break;
|
|
if (!stats[i]->finished)
|
|
allFinished = FALSE;
|
|
}
|
|
str = i<n ? mixed : activityString (activity, allFinished);
|
|
}
|
|
stateString = str;
|
|
gtr_label_set_text (GTK_LABEL (di->state_lb), str);
|
|
|
|
|
|
/* date started */
|
|
if (n<=0)
|
|
str = no_torrent;
|
|
else {
|
|
const time_t baseline = stats[0]->startDate;
|
|
for (i=1; i<n; ++i)
|
|
if (baseline != stats[i]->startDate)
|
|
break;
|
|
if (i != n)
|
|
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 {
|
|
const int baseline = stats[0]->eta;
|
|
for (i=1; i<n; ++i)
|
|
if (baseline != stats[i]->eta)
|
|
break;
|
|
if (i!=n)
|
|
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 (i=0; i<n; ++i) {
|
|
size += infos[i]->totalSize;
|
|
pieces += infos[i]->pieceCount;
|
|
if (!pieceSize)
|
|
pieceSize = infos[i]->pieceSize;
|
|
else if (pieceSize != (int)infos[i]->pieceSize)
|
|
pieceSize = -1;
|
|
}
|
|
tr_strlsize (sizebuf, size, sizeof (sizebuf));
|
|
if (!size)
|
|
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 (i=0; i<n; ++i) {
|
|
const tr_stat * st = stats[i];
|
|
haveUnchecked += st->haveUnchecked;
|
|
haveValid += st->haveValid;
|
|
sizeWhenDone += st->sizeWhenDone;
|
|
leftUntilDone += st->leftUntilDone;
|
|
available += st->sizeWhenDone - st->leftUntilDone + st->desiredAvailable;
|
|
}
|
|
{
|
|
char buf2[32], unver[64], total[64], avail[32];
|
|
const double d = sizeWhenDone ? (100.0 * available) / sizeWhenDone : 0;
|
|
const double ratio = 100.0 * (sizeWhenDone ? (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 && !leftUntilDone)
|
|
g_snprintf (buf, sizeof (buf), _("%1$s (%2$s%%)"), total, buf2);
|
|
else if (!haveUnchecked)
|
|
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], fbuf[64];
|
|
uint64_t d=0, f=0;
|
|
for (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)
|
|
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 (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 {
|
|
const char * baseline = stats[0]->errorString;
|
|
for (i=1; i<n; ++i)
|
|
if (strcmp (baseline, stats[i]->errorString))
|
|
break;
|
|
if (i==n)
|
|
str = baseline;
|
|
else
|
|
str = mixed;
|
|
}
|
|
if (!str || !*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 (i=0; i<n; ++i)
|
|
if (latest < stats[i]->activityDate)
|
|
latest = stats[i]->activityDate;
|
|
if (latest <= 0)
|
|
str = _("Never");
|
|
else {
|
|
const int 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, *w, *fr, *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);
|
|
|
|
/* 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);
|
|
|
|
/* 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);
|
|
|
|
/* 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);
|
|
gtk_misc_set_alignment (GTK_MISC (w), 0.0f, 0.0f);
|
|
|
|
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 const char*
|
|
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 const char*
|
|
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,
|
|
const char * key,
|
|
const char * torrentName,
|
|
const tr_peer_stat * peer)
|
|
{
|
|
int q[4];
|
|
char collated_name[128];
|
|
const char * client = peer->client;
|
|
|
|
if (!client || !strcmp (client, "Unknown Client"))
|
|
client = "";
|
|
|
|
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,
|
|
const tr_peer_stat * 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' };
|
|
|
|
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 i;
|
|
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 (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 (i=0; i<n; ++i) {
|
|
int j;
|
|
const tr_torrent * tor = torrents[i];
|
|
for (j=0; j<peerCount[i]; ++j) {
|
|
const tr_peer_stat * s = &peers[i][j];
|
|
char key[128];
|
|
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 (i=0; i<n; ++i) {
|
|
int j;
|
|
const tr_torrent * tor = torrents[i];
|
|
for (j=0; j<peerCount[i]; ++j) {
|
|
const tr_peer_stat * s = &peers[i][j];
|
|
char key[128];
|
|
GtkTreeRowReference * ref;
|
|
GtkTreePath * p;
|
|
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 (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 i;
|
|
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 (i=0; i<n; ++i) {
|
|
int j;
|
|
const tr_torrent * tor = torrents[i];
|
|
const tr_info * inf = tr_torrentInfo (tor);
|
|
total += inf->webseedCount;
|
|
for (j=0; j<inf->webseedCount; ++j) {
|
|
char key[256];
|
|
const char * 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 (i=0; i<n; ++i) {
|
|
int j;
|
|
const tr_torrent * tor = torrents[i];
|
|
const tr_info * inf = tr_torrentInfo (tor);
|
|
double * speeds_KBps = tr_torrentWebSpeeds_KBps (tor);
|
|
for (j=0; j<inf->webseedCount; ++j) {
|
|
char buf[128];
|
|
char key[256];
|
|
const char * url = inf->webseeds[j];
|
|
GtkTreePath * p;
|
|
GtkTreeRowReference * ref;
|
|
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 */
|
|
if (total > 0)
|
|
gtk_widget_show (di->webseed_view);
|
|
else
|
|
gtk_widget_hide (di->webseed_view);
|
|
}
|
|
|
|
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)
|
|
{
|
|
gboolean show_tip = FALSE;
|
|
GtkTreeModel * model;
|
|
GtkTreeIter iter;
|
|
|
|
if (gtk_tree_view_get_tooltip_context (GTK_TREE_VIEW (widget),
|
|
&x, &y, keyboard_tip,
|
|
&model, NULL, &iter))
|
|
{
|
|
struct DetailsImpl * di = gdi;
|
|
const char * pch;
|
|
char * name = NULL;
|
|
char * addr = NULL;
|
|
char * markup = NULL;
|
|
char * flagstr = NULL;
|
|
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, "<b>%s</b>\n%s\n \n", markup, addr);
|
|
g_free (markup);
|
|
|
|
for (pch = flagstr; pch && *pch; ++pch)
|
|
{
|
|
const char * 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)
|
|
g_string_append_printf (gstr, "%c: %s\n", *pch, s);
|
|
}
|
|
if (gstr->len) /* 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 i;
|
|
int n = 0;
|
|
const bool more = gtr_pref_flag_get (PREF_KEY_SHOW_MORE_PEER_INFO);
|
|
int view_columns[32];
|
|
GtkTreeViewColumn * c;
|
|
GtkCellRenderer * r;
|
|
|
|
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)))
|
|
gtk_tree_view_remove_column (peer_view, c);
|
|
|
|
for (i=0; i<n; ++i)
|
|
{
|
|
const int col = view_columns[i];
|
|
const char * 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. */
|
|
{
|
|
GtkTreeViewColumn *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)
|
|
{
|
|
const char * key = PREF_KEY_SHOW_MORE_PEER_INFO;
|
|
const gboolean 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;
|
|
const char * str;
|
|
GtkListStore *store;
|
|
GtkWidget *v, *w, *ret, *sw, *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);
|
|
gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (v), TRUE);
|
|
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 (PREF_KEY_SHOW_MORE_PEER_INFO);
|
|
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, const char * key, const tr_tracker_stat * st, gboolean showScrape)
|
|
{
|
|
char * str;
|
|
char timebuf[256];
|
|
const time_t now = time (NULL);
|
|
const char * err_markup_begin = "<span color=\"red\">";
|
|
const char * err_markup_end = "</span>";
|
|
const char * timeout_markup_begin = "<span color=\"#224466\">";
|
|
const char * timeout_markup_end = "</span>";
|
|
const char * success_markup_begin = "<span color=\"#008B00\">";
|
|
const char * success_markup_end = "</span>";
|
|
|
|
/* hostname */
|
|
{
|
|
g_string_append (gstr, st->isBackup ? "<i>" : "<b>");
|
|
if (key)
|
|
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 ? "</i>" : "</b>");
|
|
}
|
|
|
|
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… <small>%s</small>"), 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… <small>%s</small>"), 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 (torrent_id < 0)
|
|
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)
|
|
{
|
|
const int 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 i;
|
|
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);
|
|
const gboolean 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 (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 (i=0; i<n; ++i) {
|
|
int j;
|
|
const int jn = statCount[i];
|
|
for (j=0; j<jn; ++j) {
|
|
const tr_torrent * tor = torrents[i];
|
|
const tr_tracker_stat * st = &stats[i][j];
|
|
const int 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;
|
|
GtkTreeIter iter;
|
|
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 (i=0; i<n; ++i)
|
|
{
|
|
int j;
|
|
const tr_torrent * tor = torrents[i];
|
|
const char * summary_name = n>1 ? tr_torrentName (tor) : NULL;
|
|
for (j=0; j<statCount[i]; ++j)
|
|
{
|
|
GtkTreePath * p;
|
|
GtkTreeRowReference * ref;
|
|
const tr_tracker_stat * 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 (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)
|
|
{
|
|
const char * key = PREF_KEY_SHOW_MORE_TRACKER_INFO;
|
|
const gboolean 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)
|
|
{
|
|
const char * key = PREF_KEY_SHOW_BACKUP_TRACKERS;
|
|
const gboolean 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 i, n;
|
|
int tier;
|
|
GtkTextIter start, end;
|
|
const int 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);
|
|
for (i=0; tracker_strings[i];)
|
|
++i;
|
|
trackers = g_new0 (tr_tracker_info, i);
|
|
for (i=n=tier=0; tracker_strings[i]; ++i) {
|
|
const char * str = tracker_strings[i];
|
|
if (!*str)
|
|
++tier;
|
|
else {
|
|
trackers[n].tier = tier;
|
|
trackers[n].announce = tracker_strings[i];
|
|
++n;
|
|
}
|
|
}
|
|
|
|
/* update the torrent */
|
|
if (tr_torrentSetAnnounceList (tor, trackers, n))
|
|
refresh (di);
|
|
else {
|
|
GtkWidget * w;
|
|
const char * 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, const tr_torrent * tor)
|
|
{
|
|
int i;
|
|
int tier = 0;
|
|
const tr_info * inf = tr_torrentInfo (tor);
|
|
for (i=0; i<inf->trackerCount; ++i) {
|
|
const tr_tracker_info * 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, *d, *fr, *t, *l, *sw;
|
|
GtkWindow * win = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (button)));
|
|
GString * gstr = di->gstr; /* buffer for temporary strings */
|
|
const int 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,
|
|
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
|
|
GTK_STOCK_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);
|
|
gtk_misc_set_alignment (GTK_MISC (l), 0.0, 0.5);
|
|
hig_workarea_add_wide_control (t, &row, l);
|
|
|
|
w = gtk_text_view_new ();
|
|
gtk_widget_set_size_request (w, 500u, 166u);
|
|
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);
|
|
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;
|
|
const int 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));
|
|
const int 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 (url && *url)
|
|
{
|
|
if (tr_urlIsValidTracker (url))
|
|
{
|
|
char * json = g_strdup_printf (
|
|
"{\n"
|
|
" \"method\": \"torrent-set\",\n"
|
|
" \"arguments\": { \"id\": %d, \"trackerAdd\": [ \"%s\" ] }\n"
|
|
"}\n",
|
|
torrent_id, url);
|
|
gtr_core_exec_json (di->core, json);
|
|
refresh (di);
|
|
g_free (json);
|
|
}
|
|
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 UNUSED, gpointer gdi)
|
|
{
|
|
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,
|
|
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
|
|
GTK_STOCK_ADD, GTK_RESPONSE_ACCEPT,
|
|
NULL);
|
|
gtk_dialog_set_alternative_button_order (GTK_DIALOG (w),
|
|
GTK_RESPONSE_ACCEPT,
|
|
GTK_RESPONSE_CANCEL,
|
|
-1);
|
|
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 UNUSED, gpointer gdi)
|
|
{
|
|
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))
|
|
{
|
|
char * json;
|
|
int torrent_id;
|
|
int tracker_id;
|
|
gtk_tree_model_get (model, &iter, TRACKER_COL_TRACKER_ID, &tracker_id,
|
|
TRACKER_COL_TORRENT_ID, &torrent_id,
|
|
-1);
|
|
json = g_strdup_printf ("{\n"
|
|
" \"method\": \"torrent-set\",\n"
|
|
" \"arguments\": { \"id\": %d, \"trackerRemove\": [ %d ] }\n"
|
|
"}\n",
|
|
torrent_id, tracker_id);
|
|
gtr_core_exec_json (di->core, json);
|
|
refresh (di);
|
|
g_free (json);
|
|
}
|
|
}
|
|
|
|
static GtkWidget*
|
|
tracker_page_new (struct DetailsImpl * di)
|
|
{
|
|
gboolean b;
|
|
GtkCellRenderer * r;
|
|
GtkTreeViewColumn * c;
|
|
GtkTreeSelection * sel;
|
|
GtkWidget *vbox, *sw, *w, *v, *hbox;
|
|
const int 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);
|
|
gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (v), TRUE);
|
|
|
|
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"));
|
|
gtk_button_set_image (GTK_BUTTON (w), gtk_image_new_from_stock (GTK_STOCK_EDIT, GTK_ICON_SIZE_BUTTON));
|
|
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 (PREF_KEY_SHOW_MORE_TRACKER_INFO);
|
|
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 (PREF_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 TRUE;
|
|
}
|
|
|
|
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, *n, *v, *w, *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,
|
|
GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
|
|
NULL);
|
|
di->dialog = d;
|
|
gtk_window_set_role (GTK_WINDOW (d), "tr-info");
|
|
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];
|
|
const int 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)
|
|
{
|
|
const int id = GPOINTER_TO_INT (ids->data);
|
|
tr_torrent * tor = gtr_core_find_torrent (di->core, id);
|
|
const tr_info * 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);
|
|
}
|