1 /*
2  * This file Copyright (C) 2007-2014 Mnemosyne LLC
3  *
4  * It may be used under the GNU GPL versions 2 or 3
5  * or any future license endorsed by Mnemosyne LLC.
6  *
7  */
8 
9 #include <glib/gi18n.h>
10 #include <gtk/gtk.h>
11 
12 #include <libtransmission/transmission.h>
13 #include <libtransmission/makemeta.h>
14 #include <libtransmission/utils.h> /* tr_formatter_mem_B() */
15 
16 #include "hig.h"
17 #include "makemeta-ui.h"
18 #include "tr-core.h"
19 #include "tr-prefs.h"
20 #include "util.h"
21 
22 #define FILE_CHOSEN_KEY "file-is-chosen"
23 
24 typedef struct
25 {
26     char* target;
27     guint progress_tag;
28     GtkWidget* file_radio;
29     GtkWidget* file_chooser;
30     GtkWidget* folder_radio;
31     GtkWidget* folder_chooser;
32     GtkWidget* pieces_lb;
33     GtkWidget* destination_chooser;
34     GtkWidget* comment_check;
35     GtkWidget* comment_entry;
36     GtkWidget* private_check;
37     GtkWidget* progress_label;
38     GtkWidget* progress_bar;
39     GtkWidget* progress_dialog;
40     GtkWidget* dialog;
41     GtkTextBuffer* announce_text_buffer;
42     TrCore* core;
43     tr_metainfo_builder* builder;
44 }
45 MakeMetaUI;
46 
freeMetaUI(gpointer p)47 static void freeMetaUI(gpointer p)
48 {
49     MakeMetaUI* ui = p;
50 
51     tr_metaInfoBuilderFree(ui->builder);
52     g_free(ui->target);
53     memset(ui, ~0, sizeof(MakeMetaUI));
54     g_free(ui);
55 }
56 
onProgressDialogRefresh(gpointer data)57 static gboolean onProgressDialogRefresh(gpointer data)
58 {
59     char* str = NULL;
60     MakeMetaUI* ui = data;
61     tr_metainfo_builder const* b = ui->builder;
62     GtkDialog* d = GTK_DIALOG(ui->progress_dialog);
63     GtkProgressBar* p = GTK_PROGRESS_BAR(ui->progress_bar);
64     double const fraction = b->pieceCount != 0 ? (double)b->pieceIndex / b->pieceCount : 0;
65     char* base = g_path_get_basename(b->top);
66 
67     /* progress label */
68     if (!b->isDone)
69     {
70         str = g_strdup_printf(_("Creating \"%s\""), base);
71     }
72     else if (b->result == TR_MAKEMETA_OK)
73     {
74         str = g_strdup_printf(_("Created \"%s\"!"), base);
75     }
76     else if (b->result == TR_MAKEMETA_URL)
77     {
78         str = g_strdup_printf(_("Error: invalid announce URL \"%s\""), b->errfile);
79     }
80     else if (b->result == TR_MAKEMETA_CANCELLED)
81     {
82         str = g_strdup_printf(_("Cancelled"));
83     }
84     else if (b->result == TR_MAKEMETA_IO_READ)
85     {
86         str = g_strdup_printf(_("Error reading \"%s\": %s"), b->errfile, g_strerror(b->my_errno));
87     }
88     else if (b->result == TR_MAKEMETA_IO_WRITE)
89     {
90         str = g_strdup_printf(_("Error writing \"%s\": %s"), b->errfile, g_strerror(b->my_errno));
91     }
92     else
93     {
94         g_assert_not_reached();
95     }
96 
97     if (str != NULL)
98     {
99         gtr_label_set_text(GTK_LABEL(ui->progress_label), str);
100         g_free(str);
101     }
102 
103     /* progress bar */
104     if (b->pieceIndex == 0)
105     {
106         str = g_strdup("");
107     }
108     else
109     {
110         char sizebuf[128];
111         tr_strlsize(sizebuf, (uint64_t)b->pieceIndex * (uint64_t)b->pieceSize, sizeof(sizebuf));
112         /* how much data we've scanned through to generate checksums */
113         str = g_strdup_printf(_("Scanned %s"), sizebuf);
114     }
115 
116     gtk_progress_bar_set_fraction(p, fraction);
117     gtk_progress_bar_set_text(p, str);
118     g_free(str);
119 
120     /* buttons */
121     gtk_dialog_set_response_sensitive(d, GTK_RESPONSE_CANCEL, !b->isDone);
122     gtk_dialog_set_response_sensitive(d, GTK_RESPONSE_CLOSE, b->isDone);
123     gtk_dialog_set_response_sensitive(d, GTK_RESPONSE_ACCEPT, b->isDone && !b->result);
124 
125     g_free(base);
126     return G_SOURCE_CONTINUE;
127 }
128 
onProgressDialogDestroyed(gpointer data,GObject * dead UNUSED)129 static void onProgressDialogDestroyed(gpointer data, GObject* dead UNUSED)
130 {
131     MakeMetaUI* ui = data;
132     g_source_remove(ui->progress_tag);
133 }
134 
addTorrent(MakeMetaUI * ui)135 static void addTorrent(MakeMetaUI* ui)
136 {
137     char* path;
138     tr_metainfo_builder const* b = ui->builder;
139     tr_ctor* ctor = tr_ctorNew(gtr_core_session(ui->core));
140 
141     tr_ctorSetMetainfoFromFile(ctor, ui->target);
142 
143     path = g_path_get_dirname(b->top);
144     tr_ctorSetDownloadDir(ctor, TR_FORCE, path);
145     g_free(path);
146 
147     gtr_core_add_ctor(ui->core, ctor);
148 }
149 
onProgressDialogResponse(GtkDialog * d,int response,gpointer data)150 static void onProgressDialogResponse(GtkDialog* d, int response, gpointer data)
151 {
152     MakeMetaUI* ui = data;
153 
154     switch (response)
155     {
156     case GTK_RESPONSE_CANCEL:
157         ui->builder->abortFlag = TRUE;
158         gtk_widget_destroy(GTK_WIDGET(d));
159         break;
160 
161     case GTK_RESPONSE_ACCEPT:
162         addTorrent(ui);
163 
164     /* fall-through */
165 
166     case GTK_RESPONSE_CLOSE:
167         gtk_widget_destroy(ui->builder->result ? GTK_WIDGET(d) : ui->dialog);
168         break;
169 
170     default:
171         g_assert(0 && "unhandled response");
172     }
173 }
174 
makeProgressDialog(GtkWidget * parent,MakeMetaUI * ui)175 static void makeProgressDialog(GtkWidget* parent, MakeMetaUI* ui)
176 {
177     GtkWidget* d;
178     GtkWidget* l;
179     GtkWidget* w;
180     GtkWidget* v;
181     GtkWidget* fr;
182 
183     d = gtk_dialog_new_with_buttons(_("New Torrent"), GTK_WINDOW(parent), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
184         GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, GTK_STOCK_ADD, GTK_RESPONSE_ACCEPT, NULL);
185     ui->progress_dialog = d;
186     g_signal_connect(d, "response", G_CALLBACK(onProgressDialogResponse), ui);
187 
188     fr = gtk_frame_new(NULL);
189     gtk_container_set_border_width(GTK_CONTAINER(fr), GUI_PAD_BIG);
190     gtk_frame_set_shadow_type(GTK_FRAME(fr), GTK_SHADOW_NONE);
191     v = gtk_box_new(GTK_ORIENTATION_VERTICAL, GUI_PAD);
192     gtk_container_add(GTK_CONTAINER(fr), v);
193 
194     l = gtk_label_new(_("Creating torrent…"));
195     g_object_set(l, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_CENTER, NULL);
196     gtk_label_set_justify(GTK_LABEL(l), GTK_JUSTIFY_LEFT);
197     ui->progress_label = l;
198     gtk_box_pack_start(GTK_BOX(v), l, FALSE, FALSE, 0);
199 
200     w = gtk_progress_bar_new();
201     ui->progress_bar = w;
202     gtk_box_pack_start(GTK_BOX(v), w, FALSE, FALSE, 0);
203 
204     ui->progress_tag = gdk_threads_add_timeout_seconds(SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS, onProgressDialogRefresh, ui);
205     g_object_weak_ref(G_OBJECT(d), onProgressDialogDestroyed, ui);
206     onProgressDialogRefresh(ui);
207 
208     gtr_dialog_set_content(GTK_DIALOG(d), fr);
209     gtk_widget_show(d);
210 }
211 
onResponse(GtkDialog * d,int response,gpointer user_data)212 static void onResponse(GtkDialog* d, int response, gpointer user_data)
213 {
214     MakeMetaUI* ui = user_data;
215 
216     if (response == GTK_RESPONSE_ACCEPT)
217     {
218         if (ui->builder != NULL)
219         {
220             int n;
221             int tier;
222             GtkTextIter start;
223             GtkTextIter end;
224             char* dir;
225             char* base;
226             char* tracker_text;
227             char** tracker_strings;
228             GtkEntry* c_entry = GTK_ENTRY(ui->comment_entry);
229             GtkToggleButton* p_check = GTK_TOGGLE_BUTTON(ui->private_check);
230             GtkToggleButton* c_check = GTK_TOGGLE_BUTTON(ui->comment_check);
231             char const* comment = gtk_entry_get_text(c_entry);
232             gboolean const isPrivate = gtk_toggle_button_get_active(p_check);
233             gboolean const useComment = gtk_toggle_button_get_active(c_check);
234             tr_tracker_info* trackers;
235 
236             /* destination file */
237             dir = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(ui->destination_chooser));
238             base = g_path_get_basename(ui->builder->top);
239             g_free(ui->target);
240             ui->target = g_strdup_printf("%s/%s.torrent", dir, base);
241 
242             /* build the array of trackers */
243             gtk_text_buffer_get_bounds(ui->announce_text_buffer, &start, &end);
244             tracker_text = gtk_text_buffer_get_text(ui->announce_text_buffer, &start, &end, FALSE);
245             tracker_strings = g_strsplit(tracker_text, "\n", 0);
246 
247             trackers = g_new0(tr_tracker_info, g_strv_length(tracker_strings));
248             n = 0;
249             tier = 0;
250 
251             for (int i = 0; tracker_strings[i] != NULL; ++i)
252             {
253                 char* const str = tracker_strings[i];
254 
255                 if (tr_str_is_empty(str))
256                 {
257                     ++tier;
258                 }
259                 else
260                 {
261                     trackers[n].tier = tier;
262                     trackers[n].announce = str;
263                     ++n;
264                 }
265             }
266 
267             /* build the .torrent */
268             makeProgressDialog(GTK_WIDGET(d), ui);
269             tr_makeMetaInfo(ui->builder, ui->target, trackers, n, useComment ? comment : NULL, isPrivate);
270 
271             /* cleanup */
272             g_free(trackers);
273             g_strfreev(tracker_strings);
274             g_free(tracker_text);
275             g_free(base);
276             g_free(dir);
277         }
278     }
279     else if (response == GTK_RESPONSE_CLOSE)
280     {
281         gtk_widget_destroy(GTK_WIDGET(d));
282     }
283 }
284 
285 /***
286 ****
287 ***/
288 
onSourceToggled(GtkToggleButton * tb,gpointer user_data)289 static void onSourceToggled(GtkToggleButton* tb, gpointer user_data)
290 {
291     gtk_widget_set_sensitive(GTK_WIDGET(user_data), gtk_toggle_button_get_active(tb));
292 }
293 
updatePiecesLabel(MakeMetaUI * ui)294 static void updatePiecesLabel(MakeMetaUI* ui)
295 {
296     tr_metainfo_builder const* builder = ui->builder;
297     char const* filename = builder != NULL ? builder->top : NULL;
298     GString* gstr = g_string_new(NULL);
299 
300     g_string_append(gstr, "<i>");
301 
302     if (filename == NULL)
303     {
304         g_string_append(gstr, _("No source selected"));
305     }
306     else
307     {
308         char buf[128];
309         tr_strlsize(buf, builder->totalSize, sizeof(buf));
310         g_string_append_printf(gstr, ngettext("%1$s; %2$'d File", "%1$s; %2$'d Files", builder->fileCount), buf,
311             builder->fileCount);
312         g_string_append(gstr, "; ");
313 
314         tr_formatter_mem_B(buf, builder->pieceSize, sizeof(buf));
315         g_string_append_printf(gstr, ngettext("%1$'d Piece @ %2$s", "%1$'d Pieces @ %2$s", builder->pieceCount),
316             builder->pieceCount, buf);
317     }
318 
319     g_string_append(gstr, "</i>");
320     gtk_label_set_markup(GTK_LABEL(ui->pieces_lb), gstr->str);
321     g_string_free(gstr, TRUE);
322 }
323 
setFilename(MakeMetaUI * ui,char const * filename)324 static void setFilename(MakeMetaUI* ui, char const* filename)
325 {
326     if (ui->builder != NULL)
327     {
328         tr_metaInfoBuilderFree(ui->builder);
329         ui->builder = NULL;
330     }
331 
332     if (filename)
333     {
334         ui->builder = tr_metaInfoBuilderCreate(filename);
335     }
336 
337     updatePiecesLabel(ui);
338 }
339 
onChooserChosen(GtkFileChooser * chooser,gpointer user_data)340 static void onChooserChosen(GtkFileChooser* chooser, gpointer user_data)
341 {
342     char* filename;
343     MakeMetaUI* ui = user_data;
344 
345     g_object_set_data(G_OBJECT(chooser), FILE_CHOSEN_KEY, GINT_TO_POINTER(TRUE));
346 
347     filename = gtk_file_chooser_get_filename(chooser);
348     setFilename(ui, filename);
349     g_free(filename);
350 }
351 
onSourceToggled2(GtkToggleButton * tb,GtkWidget * chooser,MakeMetaUI * ui)352 static void onSourceToggled2(GtkToggleButton* tb, GtkWidget* chooser, MakeMetaUI* ui)
353 {
354     if (gtk_toggle_button_get_active(tb))
355     {
356         if (g_object_get_data(G_OBJECT(chooser), FILE_CHOSEN_KEY) != NULL)
357         {
358             onChooserChosen(GTK_FILE_CHOOSER(chooser), ui);
359         }
360         else
361         {
362             setFilename(ui, NULL);
363         }
364     }
365 }
366 
onFolderToggled(GtkToggleButton * tb,gpointer data)367 static void onFolderToggled(GtkToggleButton* tb, gpointer data)
368 {
369     MakeMetaUI* ui = data;
370     onSourceToggled2(tb, ui->folder_chooser, ui);
371 }
372 
onFileToggled(GtkToggleButton * tb,gpointer data)373 static void onFileToggled(GtkToggleButton* tb, gpointer data)
374 {
375     MakeMetaUI* ui = data;
376     onSourceToggled2(tb, ui->file_chooser, ui);
377 }
378 
getDefaultSavePath(void)379 static char const* getDefaultSavePath(void)
380 {
381     return g_get_user_special_dir(G_USER_DIRECTORY_DESKTOP);
382 }
383 
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 user_data)384 static void on_drag_data_received(GtkWidget* widget UNUSED, GdkDragContext* drag_context, gint x UNUSED, gint y UNUSED,
385     GtkSelectionData* selection_data, guint info UNUSED, guint time_, gpointer user_data)
386 {
387     gboolean success = FALSE;
388     MakeMetaUI* ui = user_data;
389     char** uris = gtk_selection_data_get_uris(selection_data);
390 
391     if (uris != NULL && uris[0] != NULL)
392     {
393         char const* uri = uris[0];
394         gchar* filename = g_filename_from_uri(uri, NULL, NULL);
395 
396         if (g_file_test(filename, G_FILE_TEST_IS_DIR))
397         {
398             /* a directory was dragged onto the dialog... */
399             gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->folder_radio), TRUE);
400             gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(ui->folder_chooser), filename);
401             success = TRUE;
402         }
403         else if (g_file_test(filename, G_FILE_TEST_IS_REGULAR))
404         {
405             /* a file was dragged on to the dialog... */
406             gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->file_radio), TRUE);
407             gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(ui->file_chooser), filename);
408             success = TRUE;
409         }
410 
411         g_free(filename);
412     }
413 
414     g_strfreev(uris);
415     gtk_drag_finish(drag_context, success, FALSE, time_);
416 }
417 
gtr_torrent_creation_dialog_new(GtkWindow * parent,TrCore * core)418 GtkWidget* gtr_torrent_creation_dialog_new(GtkWindow* parent, TrCore* core)
419 {
420     char const* str;
421     GtkWidget* d;
422     GtkWidget* t;
423     GtkWidget* w;
424     GtkWidget* l;
425     GtkWidget* fr;
426     GtkWidget* sw;
427     GtkWidget* v;
428     GSList* slist;
429     guint row = 0;
430     MakeMetaUI* ui = g_new0(MakeMetaUI, 1);
431 
432     ui->core = core;
433 
434     d = gtk_dialog_new_with_buttons(_("New Torrent"), parent, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CLOSE,
435         GTK_RESPONSE_CLOSE, GTK_STOCK_NEW, GTK_RESPONSE_ACCEPT, NULL);
436     ui->dialog = d;
437     g_signal_connect(d, "response", G_CALLBACK(onResponse), ui);
438     g_object_set_data_full(G_OBJECT(d), "ui", ui, freeMetaUI);
439 
440     t = hig_workarea_create();
441 
442     hig_workarea_add_section_title(t, &row, _("Files"));
443 
444     str = _("Sa_ve to:");
445     w = gtk_file_chooser_button_new(NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER);
446     gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(w), getDefaultSavePath());
447     ui->destination_chooser = w;
448     hig_workarea_add_row(t, &row, str, w, NULL);
449 
450     l = gtk_radio_button_new_with_mnemonic(NULL, _("Source F_older:"));
451     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(l), FALSE);
452     w = gtk_file_chooser_button_new(NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER);
453     g_signal_connect(l, "toggled", G_CALLBACK(onFolderToggled), ui);
454     g_signal_connect(l, "toggled", G_CALLBACK(onSourceToggled), w);
455     g_signal_connect(w, "selection-changed", G_CALLBACK(onChooserChosen), ui);
456     ui->folder_radio = l;
457     ui->folder_chooser = w;
458     gtk_widget_set_sensitive(GTK_WIDGET(w), FALSE);
459     hig_workarea_add_row_w(t, &row, l, w, NULL);
460 
461     slist = gtk_radio_button_get_group(GTK_RADIO_BUTTON(l)),
462     l = gtk_radio_button_new_with_mnemonic(slist, _("Source _File:"));
463     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(l), TRUE);
464     w = gtk_file_chooser_button_new(NULL, GTK_FILE_CHOOSER_ACTION_OPEN);
465     g_signal_connect(l, "toggled", G_CALLBACK(onFileToggled), ui);
466     g_signal_connect(l, "toggled", G_CALLBACK(onSourceToggled), w);
467     g_signal_connect(w, "selection-changed", G_CALLBACK(onChooserChosen), ui);
468     ui->file_radio = l;
469     ui->file_chooser = w;
470     hig_workarea_add_row_w(t, &row, l, w, NULL);
471 
472     w = gtk_label_new(NULL);
473     ui->pieces_lb = w;
474     gtk_label_set_markup(GTK_LABEL(w), _("<i>No source selected</i>"));
475     hig_workarea_add_row(t, &row, NULL, w, NULL);
476 
477     hig_workarea_add_section_divider(t, &row);
478     hig_workarea_add_section_title(t, &row, _("Properties"));
479 
480     str = _("_Trackers:");
481     v = gtk_box_new(GTK_ORIENTATION_VERTICAL, GUI_PAD_SMALL);
482     ui->announce_text_buffer = gtk_text_buffer_new(NULL);
483     w = gtk_text_view_new_with_buffer(ui->announce_text_buffer);
484     gtk_widget_set_size_request(w, -1, 80);
485     sw = gtk_scrolled_window_new(NULL, NULL);
486     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
487     gtk_container_add(GTK_CONTAINER(sw), w);
488     fr = gtk_frame_new(NULL);
489     gtk_frame_set_shadow_type(GTK_FRAME(fr), GTK_SHADOW_IN);
490     gtk_container_add(GTK_CONTAINER(fr), sw);
491     gtk_box_pack_start(GTK_BOX(v), fr, TRUE, TRUE, 0);
492     l = gtk_label_new(NULL);
493     gtk_label_set_markup(GTK_LABEL(l), _("To add a backup URL, add it on the line after the primary URL.\n"
494         "To add another primary URL, add it after a blank line."));
495     gtk_label_set_justify(GTK_LABEL(l), GTK_JUSTIFY_LEFT);
496     g_object_set(l, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_CENTER, NULL);
497     gtk_box_pack_start(GTK_BOX(v), l, FALSE, FALSE, 0);
498     hig_workarea_add_tall_row(t, &row, str, v, NULL);
499 
500     l = gtk_check_button_new_with_mnemonic(_("Co_mment:"));
501     ui->comment_check = l;
502     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(l), FALSE);
503     w = gtk_entry_new();
504     ui->comment_entry = w;
505     gtk_widget_set_sensitive(GTK_WIDGET(w), FALSE);
506     g_signal_connect(l, "toggled", G_CALLBACK(onSourceToggled), w);
507     hig_workarea_add_row_w(t, &row, l, w, NULL);
508 
509     w = hig_workarea_add_wide_checkbutton(t, &row, _("_Private torrent"), FALSE);
510     ui->private_check = w;
511 
512     gtr_dialog_set_content(GTK_DIALOG(d), t);
513 
514     gtk_drag_dest_set(d, GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY);
515     gtk_drag_dest_add_uri_targets(d);
516     g_signal_connect(d, "drag-data-received", G_CALLBACK(on_drag_data_received), ui);
517 
518     return d;
519 }
520