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