mirror of
https://github.com/transmission/transmission
synced 2024-12-27 18:18:10 +00:00
1670 lines
50 KiB
C
1670 lines
50 KiB
C
/******************************************************************************
|
|
* Copyright (c) Transmission authors and contributors
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
* copy of this software and associated documentation files (the "Software"),
|
|
* to deal in the Software without restriction, including without limitation
|
|
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
* and/or sell copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
* DEALINGS IN THE SOFTWARE.
|
|
*****************************************************************************/
|
|
|
|
#include <locale.h>
|
|
#include <signal.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h> /* exit () */
|
|
#include <time.h>
|
|
|
|
#include <glib/gi18n.h>
|
|
#include <glib/gstdio.h>
|
|
#include <gio/gio.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <libtransmission/transmission.h>
|
|
#include <libtransmission/rpcimpl.h>
|
|
#include <libtransmission/utils.h>
|
|
#include <libtransmission/version.h>
|
|
|
|
#include "actions.h"
|
|
#include "conf.h"
|
|
#include "details.h"
|
|
#include "dialogs.h"
|
|
#include "hig.h"
|
|
#include "makemeta-ui.h"
|
|
#include "msgwin.h"
|
|
#include "notify.h"
|
|
#include "open-dialog.h"
|
|
#include "relocate.h"
|
|
#include "stats.h"
|
|
#include "tr-core.h"
|
|
#include "tr-icon.h"
|
|
#include "tr-prefs.h"
|
|
#include "tr-window.h"
|
|
#include "util.h"
|
|
|
|
#define MY_CONFIG_NAME "transmission"
|
|
#define MY_READABLE_NAME "transmission-gtk"
|
|
|
|
#define SHOW_LICENSE
|
|
static const char * LICENSE =
|
|
"Copyright 2005-2016. All code is copyrighted by the respective authors.\n"
|
|
"\n"
|
|
"Transmission can be redistributed and/or modified under the terms of the "
|
|
"GNU GPL versions 2 or 3 or by any future license endorsed by Mnemosyne LLC.\n"
|
|
"\n"
|
|
"In addition, linking to and/or using OpenSSL is allowed.\n"
|
|
"\n"
|
|
"This program is distributed in the hope that it will be useful, "
|
|
"but WITHOUT ANY WARRANTY; without even the implied warranty of "
|
|
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
|
|
"\n"
|
|
"Some of Transmission's source files have more permissive licenses. "
|
|
"Those files may, of course, be used on their own under their own terms.\n";
|
|
|
|
struct cbdata
|
|
{
|
|
char * config_dir;
|
|
gboolean start_paused;
|
|
gboolean is_iconified;
|
|
gboolean is_closing;
|
|
|
|
guint activation_count;
|
|
guint timer;
|
|
guint update_model_soon_tag;
|
|
guint refresh_actions_tag;
|
|
gpointer icon;
|
|
GtkWindow * wind;
|
|
TrCore * core;
|
|
GtkWidget * msgwin;
|
|
GtkWidget * prefs;
|
|
GSList * error_list;
|
|
GSList * duplicates_list;
|
|
GSList * details;
|
|
GtkTreeSelection * sel;
|
|
};
|
|
|
|
static void
|
|
gtr_window_present (GtkWindow * window)
|
|
{
|
|
gtk_window_present_with_time (window, gtk_get_current_event_time ());
|
|
}
|
|
|
|
/***
|
|
****
|
|
**** DETAILS DIALOGS MANAGEMENT
|
|
****
|
|
***/
|
|
|
|
static int
|
|
compare_integers (gconstpointer a, gconstpointer b)
|
|
{
|
|
return GPOINTER_TO_INT (a) - GPOINTER_TO_INT (b);
|
|
}
|
|
|
|
static char*
|
|
get_details_dialog_key (GSList * id_list)
|
|
{
|
|
GSList * l;
|
|
GSList * tmp = g_slist_sort (g_slist_copy (id_list), compare_integers);
|
|
GString * gstr = g_string_new (NULL);
|
|
|
|
for (l=tmp; l!=NULL; l=l->next)
|
|
g_string_append_printf (gstr, "%d ", GPOINTER_TO_INT (l->data));
|
|
|
|
g_slist_free (tmp);
|
|
return g_string_free (gstr, FALSE);
|
|
}
|
|
|
|
static void
|
|
get_selected_torrent_ids_foreach (GtkTreeModel * model,
|
|
GtkTreePath * p UNUSED,
|
|
GtkTreeIter * iter,
|
|
gpointer gdata)
|
|
{
|
|
int id;
|
|
GSList ** ids = gdata;
|
|
gtk_tree_model_get (model, iter, MC_TORRENT_ID, &id, -1);
|
|
*ids = g_slist_append (*ids, GINT_TO_POINTER (id));
|
|
}
|
|
static GSList*
|
|
get_selected_torrent_ids (struct cbdata * data)
|
|
{
|
|
GSList * ids = NULL;
|
|
gtk_tree_selection_selected_foreach (data->sel,
|
|
get_selected_torrent_ids_foreach,
|
|
&ids);
|
|
return ids;
|
|
}
|
|
|
|
static void
|
|
on_details_dialog_closed (gpointer gdata, GObject * dead)
|
|
{
|
|
struct cbdata * data = gdata;
|
|
|
|
data->details = g_slist_remove (data->details, dead);
|
|
}
|
|
|
|
static void
|
|
show_details_dialog_for_selected_torrents (struct cbdata * data)
|
|
{
|
|
GtkWidget * dialog = NULL;
|
|
GSList * l;
|
|
GSList * ids = get_selected_torrent_ids (data);
|
|
char * key = get_details_dialog_key (ids);
|
|
|
|
for (l=data->details; dialog==NULL && l!=NULL; l=l->next)
|
|
if (g_strcmp0 (key, g_object_get_data (l->data, "key")) == 0)
|
|
dialog = l->data;
|
|
|
|
if (dialog == NULL)
|
|
{
|
|
dialog = gtr_torrent_details_dialog_new (GTK_WINDOW (data->wind), data->core);
|
|
gtr_torrent_details_dialog_set_torrents (dialog, ids);
|
|
g_object_set_data_full (G_OBJECT (dialog), "key", g_strdup (key), g_free);
|
|
g_object_weak_ref (G_OBJECT (dialog), on_details_dialog_closed, data);
|
|
data->details = g_slist_append (data->details, dialog);
|
|
gtk_widget_show (dialog);
|
|
}
|
|
|
|
gtr_window_present (GTK_WINDOW (dialog));
|
|
g_free (key);
|
|
g_slist_free (ids);
|
|
}
|
|
|
|
/****
|
|
*****
|
|
***** ON SELECTION CHANGED
|
|
*****
|
|
****/
|
|
|
|
struct counts_data
|
|
{
|
|
int total_count;
|
|
int queued_count;
|
|
int stopped_count;
|
|
};
|
|
|
|
static void
|
|
get_selected_torrent_counts_foreach (GtkTreeModel * model, GtkTreePath * path UNUSED,
|
|
GtkTreeIter * iter, gpointer user_data)
|
|
{
|
|
int activity = 0;
|
|
struct counts_data * counts = user_data;
|
|
|
|
++counts->total_count;
|
|
|
|
gtk_tree_model_get (model, iter, MC_ACTIVITY, &activity, -1);
|
|
|
|
if ((activity == TR_STATUS_DOWNLOAD_WAIT) || (activity == TR_STATUS_SEED_WAIT))
|
|
++counts->queued_count;
|
|
|
|
if (activity == TR_STATUS_STOPPED)
|
|
++counts->stopped_count;
|
|
}
|
|
|
|
static void
|
|
get_selected_torrent_counts (struct cbdata * data, struct counts_data * counts)
|
|
{
|
|
counts->total_count = 0;
|
|
counts->queued_count = 0;
|
|
counts->stopped_count = 0;
|
|
|
|
gtk_tree_selection_selected_foreach (data->sel, get_selected_torrent_counts_foreach, counts);
|
|
}
|
|
|
|
static void
|
|
count_updatable_foreach (GtkTreeModel * model, GtkTreePath * path UNUSED,
|
|
GtkTreeIter * iter, gpointer accumulated_status)
|
|
{
|
|
tr_torrent * tor;
|
|
gtk_tree_model_get (model, iter, MC_TORRENT, &tor, -1);
|
|
*(int*)accumulated_status |= tr_torrentCanManualUpdate (tor);
|
|
}
|
|
|
|
static gboolean
|
|
refresh_actions (gpointer gdata)
|
|
{
|
|
struct cbdata * data = gdata;
|
|
|
|
if (!data->is_closing)
|
|
{
|
|
int canUpdate;
|
|
struct counts_data sel_counts;
|
|
const size_t total = gtr_core_get_torrent_count (data->core);
|
|
const size_t active = gtr_core_get_active_torrent_count (data->core);
|
|
const int torrent_count = gtk_tree_model_iter_n_children (gtr_core_model (data->core), NULL);
|
|
bool has_selection;
|
|
|
|
get_selected_torrent_counts (data, &sel_counts);
|
|
has_selection = sel_counts.total_count > 0;
|
|
|
|
gtr_action_set_sensitive ("select-all", torrent_count != 0);
|
|
gtr_action_set_sensitive ("deselect-all", torrent_count != 0);
|
|
gtr_action_set_sensitive ("pause-all-torrents", active != 0);
|
|
gtr_action_set_sensitive ("start-all-torrents", active != total);
|
|
|
|
gtr_action_set_sensitive ("torrent-stop", (sel_counts.stopped_count < sel_counts.total_count));
|
|
gtr_action_set_sensitive ("torrent-start", (sel_counts.stopped_count) > 0);
|
|
gtr_action_set_sensitive ("torrent-start-now", (sel_counts.stopped_count + sel_counts.queued_count) > 0);
|
|
gtr_action_set_sensitive ("torrent-verify", has_selection);
|
|
gtr_action_set_sensitive ("remove-torrent", has_selection);
|
|
gtr_action_set_sensitive ("delete-torrent", has_selection);
|
|
gtr_action_set_sensitive ("relocate-torrent", has_selection);
|
|
gtr_action_set_sensitive ("queue-move-top", has_selection);
|
|
gtr_action_set_sensitive ("queue-move-up", has_selection);
|
|
gtr_action_set_sensitive ("queue-move-down", has_selection);
|
|
gtr_action_set_sensitive ("queue-move-bottom", has_selection);
|
|
gtr_action_set_sensitive ("show-torrent-properties", has_selection);
|
|
gtr_action_set_sensitive ("open-torrent-folder", sel_counts.total_count == 1);
|
|
gtr_action_set_sensitive ("copy-magnet-link-to-clipboard", sel_counts.total_count == 1);
|
|
|
|
canUpdate = 0;
|
|
gtk_tree_selection_selected_foreach (data->sel, count_updatable_foreach, &canUpdate);
|
|
gtr_action_set_sensitive ("torrent-reannounce", canUpdate != 0);
|
|
}
|
|
|
|
data->refresh_actions_tag = 0;
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static void
|
|
refresh_actions_soon (gpointer gdata)
|
|
{
|
|
struct cbdata * data = gdata;
|
|
|
|
if (!data->is_closing && !data->refresh_actions_tag)
|
|
data->refresh_actions_tag = gdk_threads_add_idle (refresh_actions, data);
|
|
}
|
|
|
|
static void
|
|
on_selection_changed (GtkTreeSelection * s UNUSED, gpointer gdata)
|
|
{
|
|
refresh_actions_soon (gdata);
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
static gboolean
|
|
has_magnet_link_handler (void)
|
|
{
|
|
GAppInfo * app_info = g_app_info_get_default_for_uri_scheme ("magnet");
|
|
const gboolean has_handler = app_info != NULL;
|
|
g_clear_object (&app_info);
|
|
return has_handler;
|
|
}
|
|
|
|
static void
|
|
register_magnet_link_handler (void)
|
|
{
|
|
GError * error;
|
|
GAppInfo * app;
|
|
const char * const content_type = "x-scheme-handler/magnet";
|
|
|
|
error = NULL;
|
|
app = g_app_info_create_from_commandline ("transmission-gtk",
|
|
"transmission-gtk",
|
|
G_APP_INFO_CREATE_SUPPORTS_URIS,
|
|
&error);
|
|
g_app_info_set_as_default_for_type (app, content_type, &error);
|
|
if (error != NULL)
|
|
{
|
|
g_warning (_("Error registering Transmission as a %s handler: %s"),
|
|
content_type,
|
|
error->message);
|
|
g_error_free (error);
|
|
}
|
|
|
|
g_clear_object (&app);
|
|
}
|
|
static void
|
|
ensure_magnet_handler_exists (void)
|
|
{
|
|
if (!has_magnet_link_handler ())
|
|
register_magnet_link_handler ();
|
|
}
|
|
|
|
static void
|
|
on_main_window_size_allocated (GtkWidget * gtk_window,
|
|
GtkAllocation * alloc UNUSED,
|
|
gpointer gdata UNUSED)
|
|
{
|
|
GdkWindow * gdk_window = gtk_widget_get_window (gtk_window);
|
|
const gboolean isMaximized = (gdk_window != NULL)
|
|
&& (gdk_window_get_state (gdk_window) & GDK_WINDOW_STATE_MAXIMIZED);
|
|
|
|
gtr_pref_int_set (TR_KEY_main_window_is_maximized, isMaximized);
|
|
|
|
if (!isMaximized)
|
|
{
|
|
int x, y, w, h;
|
|
gtk_window_get_position (GTK_WINDOW (gtk_window), &x, &y);
|
|
gtk_window_get_size (GTK_WINDOW (gtk_window), &w, &h);
|
|
gtr_pref_int_set (TR_KEY_main_window_x, x);
|
|
gtr_pref_int_set (TR_KEY_main_window_y, y);
|
|
gtr_pref_int_set (TR_KEY_main_window_width, w);
|
|
gtr_pref_int_set (TR_KEY_main_window_height, h);
|
|
}
|
|
}
|
|
|
|
/***
|
|
**** listen to changes that come from RPC
|
|
***/
|
|
|
|
struct on_rpc_changed_struct
|
|
{
|
|
TrCore * core;
|
|
tr_rpc_callback_type type;
|
|
int torrent_id;
|
|
};
|
|
|
|
static gboolean
|
|
on_rpc_changed_idle (gpointer gdata)
|
|
{
|
|
tr_torrent * tor;
|
|
struct on_rpc_changed_struct * data = gdata;
|
|
|
|
switch (data->type)
|
|
{
|
|
case TR_RPC_SESSION_CLOSE:
|
|
gtr_action_activate ("quit");
|
|
break;
|
|
|
|
case TR_RPC_TORRENT_ADDED:
|
|
if ((tor = gtr_core_find_torrent (data->core, data->torrent_id)))
|
|
gtr_core_add_torrent (data->core, tor, true);
|
|
break;
|
|
|
|
case TR_RPC_TORRENT_REMOVING:
|
|
gtr_core_remove_torrent (data->core, data->torrent_id, false);
|
|
break;
|
|
|
|
case TR_RPC_TORRENT_TRASHING:
|
|
gtr_core_remove_torrent (data->core, data->torrent_id, true);
|
|
break;
|
|
|
|
case TR_RPC_SESSION_CHANGED: {
|
|
int i;
|
|
tr_variant tmp;
|
|
tr_variant * newval;
|
|
tr_variant * oldvals = gtr_pref_get_all ();
|
|
tr_quark key;
|
|
GSList * l;
|
|
GSList * changed_keys = NULL;
|
|
tr_session * session = gtr_core_session (data->core);
|
|
tr_variantInitDict (&tmp, 100);
|
|
tr_sessionGetSettings (session, &tmp);
|
|
for (i=0; tr_variantDictChild (&tmp, i, &key, &newval); ++i)
|
|
{
|
|
bool changed;
|
|
tr_variant * oldval = tr_variantDictFind (oldvals, key);
|
|
if (!oldval)
|
|
{
|
|
changed = true;
|
|
}
|
|
else
|
|
{
|
|
char * a = tr_variantToStr (oldval, TR_VARIANT_FMT_BENC, NULL);
|
|
char * b = tr_variantToStr (newval, TR_VARIANT_FMT_BENC, NULL);
|
|
changed = g_strcmp0 (a, b) != 0;
|
|
tr_free (b);
|
|
tr_free (a);
|
|
}
|
|
|
|
if (changed)
|
|
changed_keys = g_slist_append (changed_keys, GINT_TO_POINTER(key));
|
|
}
|
|
tr_sessionGetSettings (session, oldvals);
|
|
|
|
for (l=changed_keys; l!=NULL; l=l->next)
|
|
gtr_core_pref_changed (data->core, GPOINTER_TO_INT(l->data));
|
|
|
|
g_slist_free (changed_keys);
|
|
tr_variantFree (&tmp);
|
|
break;
|
|
}
|
|
|
|
case TR_RPC_TORRENT_CHANGED:
|
|
case TR_RPC_TORRENT_MOVED:
|
|
case TR_RPC_TORRENT_STARTED:
|
|
case TR_RPC_TORRENT_STOPPED:
|
|
case TR_RPC_SESSION_QUEUE_POSITIONS_CHANGED:
|
|
/* nothing interesting to do here */
|
|
break;
|
|
}
|
|
|
|
g_free (data);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static tr_rpc_callback_status
|
|
on_rpc_changed (tr_session * session G_GNUC_UNUSED,
|
|
tr_rpc_callback_type type,
|
|
struct tr_torrent * tor,
|
|
void * gdata)
|
|
{
|
|
struct cbdata * cbdata = gdata;
|
|
struct on_rpc_changed_struct * data;
|
|
|
|
data = g_new (struct on_rpc_changed_struct, 1);
|
|
data->core = cbdata->core;
|
|
data->type = type;
|
|
data->torrent_id = tr_torrentId (tor);
|
|
gdk_threads_add_idle (on_rpc_changed_idle, data);
|
|
|
|
return TR_RPC_NOREMOVE;
|
|
}
|
|
|
|
/***
|
|
**** signal handling
|
|
***/
|
|
|
|
static sig_atomic_t global_sigcount = 0;
|
|
static struct cbdata * sighandler_cbdata = NULL;
|
|
|
|
static void
|
|
signal_handler (int sig)
|
|
{
|
|
if (++global_sigcount > 1)
|
|
{
|
|
signal (sig, SIG_DFL);
|
|
raise (sig);
|
|
}
|
|
else if ((sig == SIGINT) || (sig == SIGTERM))
|
|
{
|
|
g_message (_("Got signal %d; trying to shut down cleanly. Do it again if it gets stuck."), sig);
|
|
gtr_actions_handler ("quit", sighandler_cbdata);
|
|
}
|
|
}
|
|
|
|
/****
|
|
*****
|
|
*****
|
|
****/
|
|
|
|
static void app_setup (GtkWindow * wind, struct cbdata * cbdata);
|
|
|
|
static void
|
|
on_startup (GApplication * application, gpointer user_data)
|
|
{
|
|
GError * error;
|
|
const char * str;
|
|
GtkWindow * win;
|
|
GtkUIManager * ui_manager;
|
|
tr_session * session;
|
|
struct cbdata * cbdata = user_data;
|
|
|
|
signal (SIGINT, signal_handler);
|
|
signal (SIGTERM, signal_handler);
|
|
|
|
sighandler_cbdata = cbdata;
|
|
|
|
/* ensure the directories are created */
|
|
if ((str = gtr_pref_string_get (TR_KEY_download_dir)))
|
|
g_mkdir_with_parents (str, 0777);
|
|
if ((str = gtr_pref_string_get (TR_KEY_incomplete_dir)))
|
|
g_mkdir_with_parents (str, 0777);
|
|
|
|
/* initialize the libtransmission session */
|
|
session = tr_sessionInit (cbdata->config_dir, TRUE, gtr_pref_get_all ());
|
|
|
|
gtr_pref_flag_set (TR_KEY_alt_speed_enabled, tr_sessionUsesAltSpeed (session));
|
|
gtr_pref_int_set (TR_KEY_peer_port, tr_sessionGetPeerPort (session));
|
|
cbdata->core = gtr_core_new (session);
|
|
|
|
/* init the ui manager */
|
|
error = NULL;
|
|
ui_manager = gtk_ui_manager_new ();
|
|
gtr_actions_init (ui_manager, cbdata);
|
|
gtk_ui_manager_add_ui_from_resource (ui_manager, TR_RESOURCE_PATH "transmission-ui.xml", &error);
|
|
g_assert_no_error (error);
|
|
gtk_ui_manager_ensure_update (ui_manager);
|
|
|
|
/* create main window now to be a parent to any error dialogs */
|
|
win = GTK_WINDOW (gtr_window_new (GTK_APPLICATION (application), ui_manager, cbdata->core));
|
|
g_signal_connect (win, "size-allocate", G_CALLBACK (on_main_window_size_allocated), cbdata);
|
|
g_application_hold (application);
|
|
g_object_weak_ref (G_OBJECT (win), (GWeakNotify)g_application_release, application);
|
|
app_setup (win, cbdata);
|
|
tr_sessionSetRPCCallback (session, on_rpc_changed, cbdata);
|
|
|
|
/* check & see if it's time to update the blocklist */
|
|
if (gtr_pref_flag_get (TR_KEY_blocklist_enabled))
|
|
{
|
|
if (gtr_pref_flag_get (TR_KEY_blocklist_updates_enabled))
|
|
{
|
|
const int64_t last_time = gtr_pref_int_get (TR_KEY_blocklist_date);
|
|
const int SECONDS_IN_A_WEEK = 7 * 24 * 60 * 60;
|
|
const time_t now = time (NULL);
|
|
if (last_time + SECONDS_IN_A_WEEK < now)
|
|
gtr_core_blocklist_update (cbdata->core);
|
|
}
|
|
}
|
|
|
|
/* if there's no magnet link handler registered, register us */
|
|
ensure_magnet_handler_exists ();
|
|
}
|
|
|
|
static void
|
|
on_activate (GApplication * app UNUSED, struct cbdata * cbdata)
|
|
{
|
|
cbdata->activation_count++;
|
|
|
|
/* GApplication emits an 'activate' signal when bootstrapping the primary.
|
|
* Ordinarily we handle that by presenting the main window, but if the user
|
|
* user started Transmission minimized, ignore that initial signal... */
|
|
if (cbdata->is_iconified && (cbdata->activation_count == 1))
|
|
return;
|
|
|
|
gtr_action_activate ("present-main-window");
|
|
}
|
|
|
|
static void
|
|
open_files (GSList * files, gpointer gdata)
|
|
{
|
|
struct cbdata * cbdata = gdata;
|
|
const gboolean do_start = gtr_pref_flag_get (TR_KEY_start_added_torrents) && !cbdata->start_paused;
|
|
const gboolean do_prompt = gtr_pref_flag_get (TR_KEY_show_options_window);
|
|
const gboolean do_notify = TRUE;
|
|
|
|
gtr_core_add_files (cbdata->core, files, do_start, do_prompt, do_notify);
|
|
}
|
|
|
|
static void
|
|
on_open (GApplication * application UNUSED,
|
|
GFile ** f,
|
|
gint file_count,
|
|
gchar * hint UNUSED,
|
|
gpointer gdata)
|
|
{
|
|
int i;
|
|
GSList * files = NULL;
|
|
|
|
for (i=0; i<file_count; i++)
|
|
files = g_slist_prepend (files, f[i]);
|
|
|
|
open_files (files, gdata);
|
|
|
|
g_slist_free (files);
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
int
|
|
main (int argc, char ** argv)
|
|
{
|
|
int ret;
|
|
struct stat sb;
|
|
char * application_id;
|
|
GtkApplication * app;
|
|
GOptionContext * option_context;
|
|
bool show_version = false;
|
|
GError * error = NULL;
|
|
struct cbdata cbdata;
|
|
|
|
GOptionEntry option_entries[] = {
|
|
{ "config-dir", 'g', 0, G_OPTION_ARG_FILENAME, &cbdata.config_dir, _("Where to look for configuration files"), NULL },
|
|
{ "paused", 'p', 0, G_OPTION_ARG_NONE, &cbdata.start_paused, _("Start with all torrents paused"), NULL },
|
|
{ "minimized", 'm', 0, G_OPTION_ARG_NONE, &cbdata.is_iconified, _("Start minimized in notification area"), NULL },
|
|
{ "version", 'v', 0, G_OPTION_ARG_NONE, &show_version, _("Show version number and exit"), NULL },
|
|
{ NULL, 0, 0, 0, NULL, NULL, NULL }
|
|
};
|
|
|
|
/* default settings */
|
|
memset (&cbdata, 0, sizeof (struct cbdata));
|
|
cbdata.config_dir = (char*) tr_getDefaultConfigDir (MY_CONFIG_NAME);
|
|
|
|
/* init i18n */
|
|
setlocale (LC_ALL, "");
|
|
bindtextdomain (MY_READABLE_NAME, TRANSMISSIONLOCALEDIR);
|
|
bind_textdomain_codeset (MY_READABLE_NAME, "UTF-8");
|
|
textdomain (MY_READABLE_NAME);
|
|
|
|
/* init glib/gtk */
|
|
#if !GLIB_CHECK_VERSION(2,35,4)
|
|
g_type_init ();
|
|
#endif
|
|
g_set_application_name (_("Transmission"));
|
|
|
|
/* parse the command line */
|
|
option_context = g_option_context_new (_("[torrent files or urls]"));
|
|
g_option_context_add_main_entries (option_context, option_entries, GETTEXT_PACKAGE);
|
|
g_option_context_add_group (option_context, gtk_get_option_group (FALSE));
|
|
g_option_context_set_translation_domain (option_context, GETTEXT_PACKAGE);
|
|
if (!g_option_context_parse (option_context, &argc, &argv, &error))
|
|
{
|
|
g_print (_("%s\nRun '%s --help' to see a full list of available command line options.\n"), error->message, argv[0]);
|
|
g_error_free (error);
|
|
g_option_context_free (option_context);
|
|
return 1;
|
|
}
|
|
g_option_context_free (option_context);
|
|
|
|
/* handle the trivial "version" option */
|
|
if (show_version)
|
|
{
|
|
fprintf (stderr, "%s %s\n", MY_READABLE_NAME, LONG_VERSION_STRING);
|
|
return 0;
|
|
}
|
|
|
|
gtk_window_set_default_icon_name (MY_CONFIG_NAME);
|
|
|
|
/* init the unit formatters */
|
|
tr_formatter_mem_init (mem_K, _ (mem_K_str), _ (mem_M_str), _ (mem_G_str), _ (mem_T_str));
|
|
tr_formatter_size_init (disk_K, _ (disk_K_str), _ (disk_M_str), _ (disk_G_str), _ (disk_T_str));
|
|
tr_formatter_speed_init (speed_K, _ (speed_K_str), _ (speed_M_str), _ (speed_G_str), _ (speed_T_str));
|
|
|
|
/* set up the config dir */
|
|
gtr_pref_init (cbdata.config_dir);
|
|
g_mkdir_with_parents (cbdata.config_dir, 0755);
|
|
|
|
/* init notifications */
|
|
gtr_notify_init ();
|
|
|
|
/* init the application for the specified config dir */
|
|
stat (cbdata.config_dir, &sb);
|
|
application_id = g_strdup_printf ("com.transmissionbt.transmission_%lu_%lu", (unsigned long)sb.st_dev, (unsigned long)sb.st_ino);
|
|
app = gtk_application_new (application_id, G_APPLICATION_HANDLES_OPEN);
|
|
g_signal_connect (app, "open", G_CALLBACK (on_open), &cbdata);
|
|
g_signal_connect (app, "startup", G_CALLBACK (on_startup), &cbdata);
|
|
g_signal_connect (app, "activate", G_CALLBACK (on_activate), &cbdata);
|
|
ret = g_application_run (G_APPLICATION (app), argc, argv);
|
|
g_object_unref (app);
|
|
g_free (application_id);
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
on_core_busy (TrCore * core UNUSED, gboolean busy, struct cbdata * c)
|
|
{
|
|
gtr_window_set_busy (c->wind, busy);
|
|
}
|
|
|
|
static void on_core_error (TrCore *, guint, const char *, struct cbdata *);
|
|
static void on_add_torrent (TrCore *, tr_ctor *, gpointer);
|
|
static void on_prefs_changed (TrCore * core, const tr_quark key, gpointer);
|
|
static void main_window_setup (struct cbdata * cbdata, GtkWindow * wind);
|
|
static gboolean update_model_loop (gpointer gdata);
|
|
static gboolean update_model_once (gpointer gdata);
|
|
|
|
static void
|
|
app_setup (GtkWindow * wind, struct cbdata * cbdata)
|
|
{
|
|
if (cbdata->is_iconified)
|
|
gtr_pref_flag_set (TR_KEY_show_notification_area_icon, TRUE);
|
|
|
|
gtr_actions_set_core (cbdata->core);
|
|
|
|
/* set up core handlers */
|
|
g_signal_connect (cbdata->core, "busy", G_CALLBACK (on_core_busy), cbdata);
|
|
g_signal_connect (cbdata->core, "add-error", G_CALLBACK (on_core_error), cbdata);
|
|
g_signal_connect (cbdata->core, "add-prompt", G_CALLBACK (on_add_torrent), cbdata);
|
|
g_signal_connect (cbdata->core, "prefs-changed", G_CALLBACK (on_prefs_changed), cbdata);
|
|
|
|
/* add torrents from command-line and saved state */
|
|
gtr_core_load (cbdata->core, cbdata->start_paused);
|
|
gtr_core_torrents_added (cbdata->core);
|
|
|
|
/* set up main window */
|
|
main_window_setup (cbdata, wind);
|
|
|
|
/* set up the icon */
|
|
on_prefs_changed (cbdata->core, TR_KEY_show_notification_area_icon, cbdata);
|
|
|
|
/* start model update timer */
|
|
cbdata->timer = gdk_threads_add_timeout_seconds (MAIN_WINDOW_REFRESH_INTERVAL_SECONDS, update_model_loop, cbdata);
|
|
update_model_once (cbdata);
|
|
|
|
/* either show the window or iconify it */
|
|
if (!cbdata->is_iconified)
|
|
{
|
|
gtk_widget_show (GTK_WIDGET (wind));
|
|
}
|
|
else
|
|
{
|
|
gtk_window_set_skip_taskbar_hint (cbdata->wind,
|
|
cbdata->icon != NULL);
|
|
cbdata->is_iconified = FALSE; // ensure that the next toggle iconifies
|
|
gtr_action_set_toggled ("toggle-main-window", FALSE);
|
|
}
|
|
|
|
if (!gtr_pref_flag_get (TR_KEY_user_has_given_informed_consent))
|
|
{
|
|
GtkWidget * w = gtk_message_dialog_new (GTK_WINDOW (wind),
|
|
GTK_DIALOG_DESTROY_WITH_PARENT,
|
|
GTK_MESSAGE_OTHER,
|
|
GTK_BUTTONS_NONE,
|
|
"%s",
|
|
_("Transmission is a file sharing program. When you run a torrent, its data will be made available to others by means of upload. Any content you share is your sole responsibility."));
|
|
gtk_dialog_add_button (GTK_DIALOG (w), GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT);
|
|
gtk_dialog_add_button (GTK_DIALOG (w), _("I _Agree"), GTK_RESPONSE_ACCEPT);
|
|
gtk_dialog_set_default_response (GTK_DIALOG (w), GTK_RESPONSE_ACCEPT);
|
|
switch (gtk_dialog_run (GTK_DIALOG (w)))
|
|
{
|
|
case GTK_RESPONSE_ACCEPT:
|
|
/* only show it once */
|
|
gtr_pref_flag_set (TR_KEY_user_has_given_informed_consent, TRUE);
|
|
gtk_widget_destroy (w);
|
|
break;
|
|
|
|
default:
|
|
exit (0);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
presentMainWindow (struct cbdata * cbdata)
|
|
{
|
|
GtkWindow * window = cbdata->wind;
|
|
|
|
if (cbdata->is_iconified)
|
|
{
|
|
cbdata->is_iconified = false;
|
|
|
|
gtk_window_set_skip_taskbar_hint (window, FALSE);
|
|
}
|
|
|
|
if (!gtk_widget_get_visible (GTK_WIDGET (window)))
|
|
{
|
|
gtk_window_resize (window, gtr_pref_int_get (TR_KEY_main_window_width),
|
|
gtr_pref_int_get (TR_KEY_main_window_height));
|
|
gtk_window_move (window, gtr_pref_int_get (TR_KEY_main_window_x),
|
|
gtr_pref_int_get (TR_KEY_main_window_y));
|
|
gtr_widget_set_visible (GTK_WIDGET (window), TRUE);
|
|
}
|
|
|
|
gtr_window_present (window);
|
|
gdk_window_raise (gtk_widget_get_window (GTK_WIDGET(window)));
|
|
}
|
|
|
|
static void
|
|
hideMainWindow (struct cbdata * cbdata)
|
|
{
|
|
GtkWindow * window = cbdata->wind;
|
|
gtk_window_set_skip_taskbar_hint (window, TRUE);
|
|
gtr_widget_set_visible (GTK_WIDGET (window), FALSE);
|
|
cbdata->is_iconified = true;
|
|
}
|
|
|
|
static void
|
|
toggleMainWindow (struct cbdata * cbdata)
|
|
{
|
|
if (cbdata->is_iconified)
|
|
presentMainWindow (cbdata);
|
|
else
|
|
hideMainWindow (cbdata);
|
|
}
|
|
|
|
static void on_app_exit (gpointer vdata);
|
|
|
|
static gboolean
|
|
winclose (GtkWidget * w UNUSED,
|
|
GdkEvent * event UNUSED,
|
|
gpointer gdata)
|
|
{
|
|
struct cbdata * cbdata = gdata;
|
|
|
|
if (cbdata->icon != NULL)
|
|
gtr_action_activate ("toggle-main-window");
|
|
else
|
|
on_app_exit (cbdata);
|
|
|
|
return TRUE; /* don't propagate event further */
|
|
}
|
|
|
|
static void
|
|
rowChangedCB (GtkTreeModel * model UNUSED,
|
|
GtkTreePath * path,
|
|
GtkTreeIter * iter UNUSED,
|
|
gpointer gdata)
|
|
{
|
|
struct cbdata * data = gdata;
|
|
|
|
if (gtk_tree_selection_path_is_selected (data->sel, path))
|
|
refresh_actions_soon (data);
|
|
}
|
|
|
|
static void
|
|
on_drag_data_received (GtkWidget * widget UNUSED,
|
|
GdkDragContext * drag_context,
|
|
gint x UNUSED,
|
|
gint y UNUSED,
|
|
GtkSelectionData * selection_data,
|
|
guint info UNUSED,
|
|
guint time_,
|
|
gpointer gdata)
|
|
{
|
|
guint i;
|
|
char ** uris = gtk_selection_data_get_uris (selection_data);
|
|
const guint file_count = g_strv_length (uris);
|
|
GSList * files = NULL;
|
|
|
|
for (i=0; i<file_count; ++i)
|
|
files = g_slist_prepend (files, g_file_new_for_uri (uris[i]));
|
|
|
|
open_files (files, gdata);
|
|
|
|
/* cleanup */
|
|
g_slist_foreach (files, (GFunc)g_object_unref, NULL);
|
|
g_slist_free (files);
|
|
g_strfreev (uris);
|
|
|
|
gtk_drag_finish (drag_context, true, FALSE, time_);
|
|
}
|
|
|
|
static void
|
|
main_window_setup (struct cbdata * cbdata, GtkWindow * wind)
|
|
{
|
|
GtkWidget * w;
|
|
GtkTreeModel * model;
|
|
GtkTreeSelection * sel;
|
|
|
|
g_assert (NULL == cbdata->wind);
|
|
cbdata->wind = wind;
|
|
cbdata->sel = sel = GTK_TREE_SELECTION (gtr_window_get_selection (cbdata->wind));
|
|
|
|
g_signal_connect (sel, "changed", G_CALLBACK (on_selection_changed), cbdata);
|
|
on_selection_changed (sel, cbdata);
|
|
model = gtr_core_model (cbdata->core);
|
|
g_signal_connect (model, "row-changed", G_CALLBACK (rowChangedCB), cbdata);
|
|
g_signal_connect (wind, "delete-event", G_CALLBACK (winclose), cbdata);
|
|
refresh_actions (cbdata);
|
|
|
|
/* register to handle URIs that get dragged onto our main window */
|
|
w = GTK_WIDGET (wind);
|
|
gtk_drag_dest_set (w, GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY);
|
|
gtk_drag_dest_add_uri_targets (w);
|
|
g_signal_connect (w, "drag-data-received", G_CALLBACK (on_drag_data_received), cbdata);
|
|
}
|
|
|
|
static gboolean
|
|
on_session_closed (gpointer gdata)
|
|
{
|
|
GSList * tmp;
|
|
struct cbdata * cbdata = gdata;
|
|
|
|
tmp = g_slist_copy (cbdata->details);
|
|
g_slist_foreach (tmp, (GFunc)gtk_widget_destroy, NULL);
|
|
g_slist_free (tmp);
|
|
|
|
if (cbdata->prefs)
|
|
gtk_widget_destroy (GTK_WIDGET (cbdata->prefs));
|
|
if (cbdata->wind)
|
|
gtk_widget_destroy (GTK_WIDGET (cbdata->wind));
|
|
g_object_unref (cbdata->core);
|
|
if (cbdata->icon)
|
|
g_object_unref (cbdata->icon);
|
|
g_slist_foreach (cbdata->error_list, (GFunc)g_free, NULL);
|
|
g_slist_free (cbdata->error_list);
|
|
g_slist_foreach (cbdata->duplicates_list, (GFunc)g_free, NULL);
|
|
g_slist_free (cbdata->duplicates_list);
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
struct session_close_struct
|
|
{
|
|
tr_session * session;
|
|
struct cbdata * cbdata;
|
|
};
|
|
|
|
/* since tr_sessionClose () is a blocking function,
|
|
* delegate its call to another thread here... when it's done,
|
|
* punt the GUI teardown back to the GTK+ thread */
|
|
static gpointer
|
|
session_close_threadfunc (gpointer gdata)
|
|
{
|
|
struct session_close_struct * data = gdata;
|
|
tr_sessionClose (data->session);
|
|
gdk_threads_add_idle (on_session_closed, data->cbdata);
|
|
g_free (data);
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
exit_now_cb (GtkWidget *w UNUSED, gpointer data UNUSED)
|
|
{
|
|
exit (0);
|
|
}
|
|
|
|
static void
|
|
on_app_exit (gpointer vdata)
|
|
{
|
|
GtkWidget *r, *p, *b, *w, *c;
|
|
struct cbdata * cbdata = vdata;
|
|
struct session_close_struct * session_close_data;
|
|
|
|
if (cbdata->is_closing)
|
|
return;
|
|
|
|
cbdata->is_closing = true;
|
|
|
|
/* stop the update timer */
|
|
if (cbdata->timer)
|
|
{
|
|
g_source_remove (cbdata->timer);
|
|
cbdata->timer = 0;
|
|
}
|
|
|
|
/* stop the refresh-actions timer */
|
|
if (cbdata->refresh_actions_tag)
|
|
{
|
|
g_source_remove (cbdata->refresh_actions_tag);
|
|
cbdata->refresh_actions_tag = 0;
|
|
}
|
|
|
|
c = GTK_WIDGET (cbdata->wind);
|
|
gtk_container_remove (GTK_CONTAINER (c), gtk_bin_get_child (GTK_BIN (c)));
|
|
|
|
r = gtk_alignment_new (0.5, 0.5, 0.01, 0.01);
|
|
gtk_container_add (GTK_CONTAINER (c), r);
|
|
|
|
p = gtk_grid_new ();
|
|
gtk_grid_set_column_spacing (GTK_GRID (p), GUI_PAD_BIG);
|
|
gtk_container_add (GTK_CONTAINER (r), p);
|
|
|
|
w = gtk_image_new_from_stock (GTK_STOCK_NETWORK, GTK_ICON_SIZE_DIALOG);
|
|
gtk_grid_attach (GTK_GRID (p), w, 0, 0, 1, 2);
|
|
|
|
w = gtk_label_new (NULL);
|
|
gtk_label_set_markup (GTK_LABEL (w), _("<b>Closing Connections</b>"));
|
|
gtk_misc_set_alignment (GTK_MISC (w), 0.0, 0.5);
|
|
gtk_grid_attach (GTK_GRID (p), w, 1, 0, 1, 1);
|
|
|
|
w = gtk_label_new (_("Sending upload/download totals to tracker…"));
|
|
gtk_misc_set_alignment (GTK_MISC (w), 0.0, 0.5);
|
|
gtk_grid_attach (GTK_GRID (p), w, 1, 1, 1, 1);
|
|
|
|
b = gtk_alignment_new (0.0, 1.0, 0.01, 0.01);
|
|
w = gtk_button_new_with_mnemonic (_("_Quit Now"));
|
|
g_signal_connect (w, "clicked", G_CALLBACK (exit_now_cb), NULL);
|
|
gtk_container_add (GTK_CONTAINER (b), w);
|
|
gtk_grid_attach (GTK_GRID (p), b, 1, 2, 1, 1);
|
|
|
|
gtk_widget_show_all (r);
|
|
gtk_widget_grab_focus (w);
|
|
|
|
/* clear the UI */
|
|
gtr_core_clear (cbdata->core);
|
|
|
|
/* ensure the window is in its previous position & size.
|
|
* this seems to be necessary because changing the main window's
|
|
* child seems to unset the size */
|
|
gtk_window_resize (cbdata->wind, gtr_pref_int_get (TR_KEY_main_window_width),
|
|
gtr_pref_int_get (TR_KEY_main_window_height));
|
|
gtk_window_move (cbdata->wind, gtr_pref_int_get (TR_KEY_main_window_x),
|
|
gtr_pref_int_get (TR_KEY_main_window_y));
|
|
|
|
/* shut down libT */
|
|
session_close_data = g_new (struct session_close_struct, 1);
|
|
session_close_data->cbdata = cbdata;
|
|
session_close_data->session = gtr_core_close (cbdata->core);
|
|
g_thread_new ("shutdown-thread", session_close_threadfunc, session_close_data);
|
|
}
|
|
|
|
static void
|
|
show_torrent_errors (GtkWindow * window, const char * primary, GSList ** files)
|
|
{
|
|
GSList * l;
|
|
GtkWidget * w;
|
|
GString * s = g_string_new (NULL);
|
|
const char * leader = g_slist_length (*files) > 1
|
|
? gtr_get_unicode_string (GTR_UNICODE_BULLET)
|
|
: "";
|
|
|
|
for (l=*files; l!=NULL; l=l->next)
|
|
g_string_append_printf (s, "%s %s\n", leader, (const char*)l->data);
|
|
|
|
w = gtk_message_dialog_new (window,
|
|
GTK_DIALOG_DESTROY_WITH_PARENT,
|
|
GTK_MESSAGE_ERROR,
|
|
GTK_BUTTONS_CLOSE,
|
|
"%s", primary);
|
|
gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (w),
|
|
"%s", s->str);
|
|
g_signal_connect_swapped (w, "response",
|
|
G_CALLBACK (gtk_widget_destroy), w);
|
|
gtk_widget_show (w);
|
|
g_string_free (s, TRUE);
|
|
|
|
g_slist_foreach (*files, (GFunc)g_free, NULL);
|
|
g_slist_free (*files);
|
|
*files = NULL;
|
|
}
|
|
|
|
static void
|
|
flush_torrent_errors (struct cbdata * cbdata)
|
|
{
|
|
if (cbdata->error_list)
|
|
show_torrent_errors (cbdata->wind,
|
|
ngettext ("Couldn't add corrupt torrent",
|
|
"Couldn't add corrupt torrents",
|
|
g_slist_length (cbdata->error_list)),
|
|
&cbdata->error_list);
|
|
|
|
if (cbdata->duplicates_list)
|
|
show_torrent_errors (cbdata->wind,
|
|
ngettext ("Couldn't add duplicate torrent",
|
|
"Couldn't add duplicate torrents",
|
|
g_slist_length (cbdata->duplicates_list)),
|
|
&cbdata->duplicates_list);
|
|
}
|
|
|
|
static void
|
|
on_core_error (TrCore * core UNUSED, guint code, const char * msg, struct cbdata * c)
|
|
{
|
|
switch (code)
|
|
{
|
|
case TR_PARSE_ERR:
|
|
c->error_list = g_slist_append (c->error_list, g_path_get_basename (msg));
|
|
break;
|
|
|
|
case TR_PARSE_DUPLICATE:
|
|
c->duplicates_list = g_slist_append (c->duplicates_list, g_strdup (msg));
|
|
break;
|
|
|
|
case TR_CORE_ERR_NO_MORE_TORRENTS:
|
|
flush_torrent_errors (c);
|
|
break;
|
|
|
|
default:
|
|
g_assert_not_reached ();
|
|
break;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
on_main_window_focus_in (GtkWidget * widget UNUSED,
|
|
GdkEventFocus * event UNUSED,
|
|
gpointer gdata)
|
|
{
|
|
struct cbdata * cbdata = gdata;
|
|
|
|
if (cbdata->wind)
|
|
gtk_window_set_urgency_hint (cbdata->wind, FALSE);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
on_add_torrent (TrCore * core, tr_ctor * ctor, gpointer gdata)
|
|
{
|
|
struct cbdata * cbdata = gdata;
|
|
GtkWidget * w = gtr_torrent_options_dialog_new (cbdata->wind, core, ctor);
|
|
|
|
g_signal_connect (w, "focus-in-event",
|
|
G_CALLBACK (on_main_window_focus_in), cbdata);
|
|
if (cbdata->wind)
|
|
gtk_window_set_urgency_hint (cbdata->wind, TRUE);
|
|
|
|
gtk_widget_show (w);
|
|
}
|
|
|
|
static void
|
|
on_prefs_changed (TrCore * core UNUSED, const tr_quark key, gpointer data)
|
|
{
|
|
struct cbdata * cbdata = data;
|
|
tr_session * tr = gtr_core_session (cbdata->core);
|
|
|
|
switch (key)
|
|
{
|
|
case TR_KEY_encryption:
|
|
tr_sessionSetEncryption (tr, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_download_dir:
|
|
tr_sessionSetDownloadDir (tr, gtr_pref_string_get (key));
|
|
break;
|
|
|
|
case TR_KEY_message_level:
|
|
tr_logSetLevel (gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_peer_port:
|
|
tr_sessionSetPeerPort (tr, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_blocklist_enabled:
|
|
tr_blocklistSetEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_blocklist_url:
|
|
tr_blocklistSetURL (tr, gtr_pref_string_get (key));
|
|
break;
|
|
|
|
case TR_KEY_show_notification_area_icon:
|
|
{
|
|
const bool show = gtr_pref_flag_get (key);
|
|
if (show && !cbdata->icon)
|
|
cbdata->icon = gtr_icon_new (cbdata->core);
|
|
else if (!show && cbdata->icon)
|
|
g_clear_object (&cbdata->icon);
|
|
break;
|
|
}
|
|
|
|
case TR_KEY_speed_limit_down_enabled:
|
|
tr_sessionLimitSpeed (tr, TR_DOWN, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_speed_limit_down:
|
|
tr_sessionSetSpeedLimit_KBps (tr, TR_DOWN, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_speed_limit_up_enabled:
|
|
tr_sessionLimitSpeed (tr, TR_UP, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_speed_limit_up:
|
|
tr_sessionSetSpeedLimit_KBps (tr, TR_UP, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_ratio_limit_enabled:
|
|
tr_sessionSetRatioLimited (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_ratio_limit:
|
|
tr_sessionSetRatioLimit (tr, gtr_pref_double_get (key));
|
|
break;
|
|
|
|
case TR_KEY_idle_seeding_limit:
|
|
tr_sessionSetIdleLimit (tr, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_idle_seeding_limit_enabled:
|
|
tr_sessionSetIdleLimited (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_port_forwarding_enabled:
|
|
tr_sessionSetPortForwardingEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_pex_enabled:
|
|
tr_sessionSetPexEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_rename_partial_files:
|
|
tr_sessionSetIncompleteFileNamingEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_download_queue_size:
|
|
tr_sessionSetQueueSize (tr, TR_DOWN, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_queue_stalled_minutes:
|
|
tr_sessionSetQueueStalledMinutes (tr, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_dht_enabled:
|
|
tr_sessionSetDHTEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_utp_enabled:
|
|
tr_sessionSetUTPEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_lpd_enabled:
|
|
tr_sessionSetLPDEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_rpc_port:
|
|
tr_sessionSetRPCPort (tr, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_rpc_enabled:
|
|
tr_sessionSetRPCEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_rpc_whitelist:
|
|
tr_sessionSetRPCWhitelist (tr, gtr_pref_string_get (key));
|
|
break;
|
|
|
|
case TR_KEY_rpc_whitelist_enabled:
|
|
tr_sessionSetRPCWhitelistEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_rpc_username:
|
|
tr_sessionSetRPCUsername (tr, gtr_pref_string_get (key));
|
|
break;
|
|
|
|
case TR_KEY_rpc_password:
|
|
tr_sessionSetRPCPassword (tr, gtr_pref_string_get (key));
|
|
break;
|
|
|
|
case TR_KEY_rpc_authentication_required:
|
|
tr_sessionSetRPCPasswordEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_alt_speed_up:
|
|
tr_sessionSetAltSpeed_KBps (tr, TR_UP, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_alt_speed_down:
|
|
tr_sessionSetAltSpeed_KBps (tr, TR_DOWN, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_alt_speed_enabled:
|
|
{
|
|
const bool b = gtr_pref_flag_get (key);
|
|
tr_sessionUseAltSpeed (tr, b);
|
|
gtr_action_set_toggled (tr_quark_get_string(key,NULL), b);
|
|
break;
|
|
}
|
|
|
|
case TR_KEY_alt_speed_time_begin:
|
|
tr_sessionSetAltSpeedBegin (tr, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_alt_speed_time_end:
|
|
tr_sessionSetAltSpeedEnd (tr, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_alt_speed_time_enabled:
|
|
tr_sessionUseAltSpeedTime (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_alt_speed_time_day:
|
|
tr_sessionSetAltSpeedDay (tr, gtr_pref_int_get (key));
|
|
break;
|
|
|
|
case TR_KEY_peer_port_random_on_start:
|
|
tr_sessionSetPeerPortRandomOnStart (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_incomplete_dir:
|
|
tr_sessionSetIncompleteDir (tr, gtr_pref_string_get (key));
|
|
break;
|
|
|
|
case TR_KEY_incomplete_dir_enabled:
|
|
tr_sessionSetIncompleteDirEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_script_torrent_done_enabled:
|
|
tr_sessionSetTorrentDoneScriptEnabled (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_script_torrent_done_filename:
|
|
tr_sessionSetTorrentDoneScript (tr, gtr_pref_string_get (key));
|
|
break;
|
|
|
|
case TR_KEY_start_added_torrents:
|
|
tr_sessionSetPaused (tr, !gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
case TR_KEY_trash_original_torrent_files:
|
|
tr_sessionSetDeleteSource (tr, gtr_pref_flag_get (key));
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
update_model_once (gpointer gdata)
|
|
{
|
|
struct cbdata *data = gdata;
|
|
|
|
/* update the torrent data in the model */
|
|
gtr_core_update (data->core);
|
|
|
|
/* refresh the main window's statusbar and toolbar buttons */
|
|
if (data->wind != NULL)
|
|
gtr_window_refresh (data->wind);
|
|
|
|
/* update the actions */
|
|
refresh_actions (data);
|
|
|
|
/* update the status tray icon */
|
|
if (data->icon != NULL)
|
|
gtr_icon_refresh (data->icon);
|
|
|
|
data->update_model_soon_tag = 0;
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static void
|
|
update_model_soon (gpointer gdata)
|
|
{
|
|
struct cbdata *data = gdata;
|
|
|
|
if (data->update_model_soon_tag == 0)
|
|
data->update_model_soon_tag = gdk_threads_add_idle (update_model_once, data);
|
|
}
|
|
|
|
static gboolean
|
|
update_model_loop (gpointer gdata)
|
|
{
|
|
const gboolean done = global_sigcount;
|
|
|
|
if (!done)
|
|
update_model_once (gdata);
|
|
|
|
return !done;
|
|
}
|
|
|
|
static void
|
|
show_about_dialog (GtkWindow * parent)
|
|
{
|
|
const char * uri = "https://transmissionbt.com/";
|
|
const char * authors[] = { "Jordan Lee (Backend; GTK+)",
|
|
"Mitchell Livingston (Backend; OS X)",
|
|
"Mike Gelfand",
|
|
NULL };
|
|
|
|
gtk_show_about_dialog (parent,
|
|
"authors", authors,
|
|
"comments", _("A fast and easy BitTorrent client"),
|
|
"copyright", _("Copyright (c) The Transmission Project"),
|
|
"logo-icon-name", MY_CONFIG_NAME,
|
|
"name", g_get_application_name (),
|
|
/* Translators: translate "translator-credits" as your name
|
|
to have it appear in the credits in the "About"
|
|
dialog */
|
|
"translator-credits", _("translator-credits"),
|
|
"version", LONG_VERSION_STRING,
|
|
"website", uri,
|
|
"website-label", uri,
|
|
#ifdef SHOW_LICENSE
|
|
"license", LICENSE,
|
|
"wrap-license", TRUE,
|
|
#endif
|
|
NULL);
|
|
}
|
|
|
|
static void
|
|
append_id_to_benc_list (GtkTreeModel * m, GtkTreePath * path UNUSED,
|
|
GtkTreeIter * iter, gpointer list)
|
|
{
|
|
tr_torrent * tor = NULL;
|
|
gtk_tree_model_get (m, iter, MC_TORRENT, &tor, -1);
|
|
tr_variantListAddInt (list, tr_torrentId (tor));
|
|
}
|
|
|
|
static gboolean
|
|
call_rpc_for_selected_torrents (struct cbdata * data, const char * method)
|
|
{
|
|
tr_variant top, *args, *ids;
|
|
gboolean invoked = FALSE;
|
|
GtkTreeSelection * s = data->sel;
|
|
tr_session * session = gtr_core_session (data->core);
|
|
|
|
tr_variantInitDict (&top, 2);
|
|
tr_variantDictAddStr (&top, TR_KEY_method, method);
|
|
args = tr_variantDictAddDict (&top, TR_KEY_arguments, 1);
|
|
ids = tr_variantDictAddList (args, TR_KEY_ids, 0);
|
|
gtk_tree_selection_selected_foreach (s, append_id_to_benc_list, ids);
|
|
|
|
if (tr_variantListSize (ids) != 0)
|
|
{
|
|
tr_rpc_request_exec_json (session, &top, NULL, NULL);
|
|
invoked = TRUE;
|
|
}
|
|
|
|
tr_variantFree (&top);
|
|
return invoked;
|
|
}
|
|
|
|
static void
|
|
open_folder_foreach (GtkTreeModel * model, GtkTreePath * path UNUSED,
|
|
GtkTreeIter * iter, gpointer core)
|
|
{
|
|
int id;
|
|
gtk_tree_model_get (model, iter, MC_TORRENT_ID, &id, -1);
|
|
gtr_core_open_folder (core, id);
|
|
}
|
|
|
|
static gboolean
|
|
on_message_window_closed (void)
|
|
{
|
|
gtr_action_set_toggled ("toggle-message-log", FALSE);
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
accumulate_selected_torrents (GtkTreeModel * model, GtkTreePath * path UNUSED,
|
|
GtkTreeIter * iter, gpointer gdata)
|
|
{
|
|
int id;
|
|
GSList ** data = gdata;
|
|
|
|
gtk_tree_model_get (model, iter, MC_TORRENT_ID, &id, -1);
|
|
*data = g_slist_append (*data, GINT_TO_POINTER (id));
|
|
}
|
|
|
|
static void
|
|
remove_selected (struct cbdata * data, gboolean delete_files)
|
|
{
|
|
GSList * l = NULL;
|
|
|
|
gtk_tree_selection_selected_foreach (data->sel, accumulate_selected_torrents, &l);
|
|
|
|
if (l != NULL)
|
|
gtr_confirm_remove (data->wind, data->core, l, delete_files);
|
|
}
|
|
|
|
static void
|
|
start_all_torrents (struct cbdata * data)
|
|
{
|
|
tr_session * session = gtr_core_session (data->core);
|
|
tr_variant request;
|
|
|
|
tr_variantInitDict (&request, 1);
|
|
tr_variantDictAddStr (&request, TR_KEY_method, "torrent-start");
|
|
tr_rpc_request_exec_json (session, &request, NULL, NULL);
|
|
tr_variantFree (&request);
|
|
}
|
|
|
|
static void
|
|
pause_all_torrents (struct cbdata * data)
|
|
{
|
|
tr_session * session = gtr_core_session (data->core);
|
|
tr_variant request;
|
|
|
|
tr_variantInitDict (&request, 1);
|
|
tr_variantDictAddStr (&request, TR_KEY_method, "torrent-stop");
|
|
tr_rpc_request_exec_json (session, &request, NULL, NULL);
|
|
tr_variantFree (&request);
|
|
}
|
|
|
|
static tr_torrent*
|
|
get_first_selected_torrent (struct cbdata * data)
|
|
{
|
|
tr_torrent * tor = NULL;
|
|
GtkTreeModel * m;
|
|
GList * l = gtk_tree_selection_get_selected_rows (data->sel, &m);
|
|
if (l != NULL)
|
|
{
|
|
GtkTreePath * p = l->data;
|
|
GtkTreeIter i;
|
|
if (gtk_tree_model_get_iter (m, &i, p))
|
|
gtk_tree_model_get (m, &i, MC_TORRENT, &tor, -1);
|
|
}
|
|
g_list_foreach (l, (GFunc)gtk_tree_path_free, NULL);
|
|
g_list_free (l);
|
|
return tor;
|
|
}
|
|
|
|
static void
|
|
copy_magnet_link_to_clipboard (GtkWidget * w, tr_torrent * tor)
|
|
{
|
|
char * magnet = tr_torrentGetMagnetLink (tor);
|
|
GdkDisplay * display = gtk_widget_get_display (w);
|
|
GdkAtom selection;
|
|
GtkClipboard * clipboard;
|
|
|
|
/* this is The Right Thing for copy/paste... */
|
|
selection = GDK_SELECTION_CLIPBOARD;
|
|
clipboard = gtk_clipboard_get_for_display (display, selection);
|
|
gtk_clipboard_set_text (clipboard, magnet, -1);
|
|
|
|
/* ...but people using plain ol' X need this instead */
|
|
selection = GDK_SELECTION_PRIMARY;
|
|
clipboard = gtk_clipboard_get_for_display (display, selection);
|
|
gtk_clipboard_set_text (clipboard, magnet, -1);
|
|
|
|
/* cleanup */
|
|
tr_free (magnet);
|
|
}
|
|
|
|
void
|
|
gtr_actions_handler (const char * action_name, gpointer user_data)
|
|
{
|
|
gboolean changed = FALSE;
|
|
struct cbdata * data = user_data;
|
|
|
|
if (g_strcmp0 (action_name, "open-torrent-from-url") == 0)
|
|
{
|
|
GtkWidget * w = gtr_torrent_open_from_url_dialog_new (data->wind, data->core);
|
|
gtk_widget_show (w);
|
|
}
|
|
else if (g_strcmp0 (action_name, "open-torrent-menu") == 0
|
|
|| g_strcmp0 (action_name, "open-torrent-toolbar") == 0)
|
|
{
|
|
GtkWidget * w = gtr_torrent_open_from_file_dialog_new (data->wind, data->core);
|
|
gtk_widget_show (w);
|
|
}
|
|
else if (g_strcmp0 (action_name, "show-stats") == 0)
|
|
{
|
|
GtkWidget * dialog = gtr_stats_dialog_new (data->wind, data->core);
|
|
gtk_widget_show (dialog);
|
|
}
|
|
else if (g_strcmp0 (action_name, "donate") == 0)
|
|
{
|
|
gtr_open_uri ("https://transmissionbt.com/donate/");
|
|
}
|
|
else if (g_strcmp0 (action_name, "pause-all-torrents") == 0)
|
|
{
|
|
pause_all_torrents (data);
|
|
}
|
|
else if (g_strcmp0 (action_name, "start-all-torrents") == 0)
|
|
{
|
|
start_all_torrents (data);
|
|
}
|
|
else if (g_strcmp0 (action_name, "copy-magnet-link-to-clipboard") == 0)
|
|
{
|
|
tr_torrent * tor = get_first_selected_torrent (data);
|
|
if (tor != NULL)
|
|
{
|
|
copy_magnet_link_to_clipboard (GTK_WIDGET (data->wind), tor);
|
|
}
|
|
}
|
|
else if (g_strcmp0 (action_name, "relocate-torrent") == 0)
|
|
{
|
|
GSList * ids = get_selected_torrent_ids (data);
|
|
if (ids != NULL)
|
|
{
|
|
GtkWindow * parent = data->wind;
|
|
GtkWidget * w = gtr_relocate_dialog_new (parent, data->core, ids);
|
|
gtk_widget_show (w);
|
|
}
|
|
}
|
|
else if (g_strcmp0 (action_name, "torrent-start") == 0
|
|
|| g_strcmp0 (action_name, "torrent-start-now") == 0
|
|
|| g_strcmp0 (action_name, "torrent-stop") == 0
|
|
|| g_strcmp0 (action_name, "torrent-reannounce") == 0
|
|
|| g_strcmp0 (action_name, "torrent-verify") == 0
|
|
|| g_strcmp0 (action_name, "queue-move-top") == 0
|
|
|| g_strcmp0 (action_name, "queue-move-up") == 0
|
|
|| g_strcmp0 (action_name, "queue-move-down") == 0
|
|
|| g_strcmp0 (action_name, "queue-move-bottom") == 0)
|
|
{
|
|
changed |= call_rpc_for_selected_torrents (data, action_name);
|
|
}
|
|
else if (g_strcmp0 (action_name, "open-torrent-folder") == 0)
|
|
{
|
|
gtk_tree_selection_selected_foreach (data->sel, open_folder_foreach, data->core);
|
|
}
|
|
else if (g_strcmp0 (action_name, "show-torrent-properties") == 0)
|
|
{
|
|
show_details_dialog_for_selected_torrents (data);
|
|
}
|
|
else if (g_strcmp0 (action_name, "new-torrent") == 0)
|
|
{
|
|
GtkWidget * w = gtr_torrent_creation_dialog_new (data->wind, data->core);
|
|
gtk_widget_show (w);
|
|
}
|
|
else if (g_strcmp0 (action_name, "remove-torrent") == 0)
|
|
{
|
|
remove_selected (data, FALSE);
|
|
}
|
|
else if (g_strcmp0 (action_name, "delete-torrent") == 0)
|
|
{
|
|
remove_selected (data, TRUE);
|
|
}
|
|
else if (g_strcmp0 (action_name, "quit") == 0)
|
|
{
|
|
on_app_exit (data);
|
|
}
|
|
else if (g_strcmp0 (action_name, "select-all") == 0)
|
|
{
|
|
gtk_tree_selection_select_all (data->sel);
|
|
}
|
|
else if (g_strcmp0 (action_name, "deselect-all") == 0)
|
|
{
|
|
gtk_tree_selection_unselect_all (data->sel);
|
|
}
|
|
else if (g_strcmp0 (action_name, "edit-preferences") == 0)
|
|
{
|
|
if (NULL == data->prefs)
|
|
{
|
|
data->prefs = gtr_prefs_dialog_new (data->wind, G_OBJECT (data->core));
|
|
g_signal_connect (data->prefs, "destroy",
|
|
G_CALLBACK (gtk_widget_destroyed), &data->prefs);
|
|
}
|
|
gtr_window_present (GTK_WINDOW (data->prefs));
|
|
}
|
|
else if (g_strcmp0 (action_name, "toggle-message-log") == 0)
|
|
{
|
|
if (!data->msgwin)
|
|
{
|
|
GtkWidget * win = gtr_message_log_window_new (data->wind, data->core);
|
|
g_signal_connect (win, "destroy", G_CALLBACK (on_message_window_closed), NULL);
|
|
data->msgwin = win;
|
|
}
|
|
else
|
|
{
|
|
gtr_action_set_toggled ("toggle-message-log", FALSE);
|
|
gtk_widget_destroy (data->msgwin);
|
|
data->msgwin = NULL;
|
|
}
|
|
}
|
|
else if (g_strcmp0 (action_name, "show-about-dialog") == 0)
|
|
{
|
|
show_about_dialog (data->wind);
|
|
}
|
|
else if (g_strcmp0 (action_name, "help") == 0)
|
|
{
|
|
gtr_open_uri (gtr_get_help_uri ());
|
|
}
|
|
else if (g_strcmp0 (action_name, "toggle-main-window") == 0)
|
|
{
|
|
toggleMainWindow (data);
|
|
}
|
|
else if (g_strcmp0 (action_name, "present-main-window") == 0)
|
|
{
|
|
presentMainWindow (data);
|
|
}
|
|
else
|
|
{
|
|
g_error ("Unhandled action: %s", action_name);
|
|
}
|
|
|
|
if (changed)
|
|
update_model_soon (data);
|
|
}
|