1 /*
2  *  This file is part of GNOME Twitch - 'Enjoy Twitch on your GNU/Linux desktop'
3  *  Copyright © 2017 Vincent Szolnoky <vinszent@vinszent.com>
4  *
5  *  GNOME Twitch is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation, either version 3 of the License, or
8  *  (at your option) any later version.
9  *
10  *  GNOME Twitch is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with GNOME Twitch. If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "gt-win.h"
20 #include <glib/gprintf.h>
21 #include <glib/gi18n.h>
22 #include <gio/gio.h>
23 #include "gt-twitch.h"
24 #include "gt-player.h"
25 #include "gt-browse-header-bar.h"
26 #include "gt-channel-header-bar.h"
27 #include "gt-channel-container-view.h"
28 #include "gt-followed-container-view.h"
29 #include "gt-game-container-view.h"
30 #include "gt-channel-vod-container.h"
31 #include "gt-settings-dlg.h"
32 #include "gt-twitch-login-dlg.h"
33 #include "gt-twitch-channel-info-dlg.h"
34 #include "gt-chat.h"
35 #include "gt-enums.h"
36 #include "config.h"
37 #include "version.h"
38 #include "utils.h"
39 
40 #define TAG "GtWin"
41 #include "gnome-twitch/gt-log.h"
42 
43 typedef struct
44 {
45     gchar* msg;
46     GCallback cb;
47     gpointer udata;
48 } QueuedInfoData;
49 
50 typedef struct
51 {
52     GtkWidget* main_stack;
53     GtkWidget* header_stack;
54     GtkWidget* browse_stack;
55     GtkWidget* browse_header_bar;
56     GtkWidget* browse_stack_switcher;
57     GtkWidget* chat_view;
58     GtkWidget* player;
59     GtkWidget* channel_stack;
60     GtkWidget* channel_vod_container;
61     GtkWidget* channel_header_bar;
62 
63     GtkWidget* info_revealer;
64     GtkWidget* info_label;
65     GtkWidget* info_bar;
66     GtkWidget* info_bar_yes_button;
67     GtkWidget* info_bar_no_button;
68     GtkWidget* info_bar_ok_button;
69     GtkWidget* info_bar_details_button;
70     GtkWidget* info_bar_close_button;
71 
72     QueuedInfoData* cur_info_data;
73     GQueue* info_queue;
74 
75     GtSettingsDlg* settings_dlg;
76 
77     gboolean fullscreen;
78 
79     GBinding* search_binding;
80     GBinding* back_binding;
81 
82     guint inhibit_cookie;
83 } GtWinPrivate;
84 
85 G_DEFINE_TYPE_WITH_PRIVATE(GtWin, gt_win, GTK_TYPE_APPLICATION_WINDOW)
86 
87 enum
88 {
89     PROP_0,
90     PROP_FULLSCREEN,
91     PROP_OPEN_CHANNEL,
92     NUM_PROPS,
93 };
94 
95 static GParamSpec* props[NUM_PROPS];
96 
97 GtWin*
gt_win_new(GtApp * app)98 gt_win_new(GtApp* app)
99 {
100     return g_object_new(GT_TYPE_WIN,
101                         "application", app,
102                         "show-menubar", FALSE,
103                         NULL);
104 }
105 
106 static void
container_view_changed_cb(GObject * source,GParamSpec * pspec,gpointer udata)107 container_view_changed_cb(GObject* source,
108     GParamSpec* pspec, gpointer udata)
109 {
110     g_assert(GT_IS_WIN(udata));
111 
112     GtWin* self = GT_WIN(udata);
113     GtWinPrivate* priv = gt_win_get_instance_private(self);
114 
115     GtkWidget* current_view = gtk_stack_get_visible_child(GTK_STACK(priv->browse_stack));
116 
117     g_assert(GT_IS_CONTAINER_VIEW(current_view));
118 
119     g_clear_object(&priv->back_binding);
120     g_clear_object(&priv->search_binding);
121 
122     g_object_set(priv->browse_header_bar,
123         "search-active",
124         gt_container_view_get_search_active(GT_CONTAINER_VIEW(current_view)),
125         "show-back-button",
126         gt_container_view_get_show_back_button(GT_CONTAINER_VIEW(current_view)),
127         NULL);
128 
129     priv->search_binding = g_object_bind_property(priv->browse_header_bar, "search-active",
130         current_view, "search-active", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
131 
132     priv->back_binding = g_object_bind_property(current_view, "show-back-button",
133         priv->browse_header_bar, "show-back-button", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
134 }
135 
136 static void
show_about_cb(GSimpleAction * action,GVariant * par,gpointer udata)137 show_about_cb(GSimpleAction* action,
138               GVariant* par,
139               gpointer udata)
140 {
141     GtWin* self = GT_WIN(udata);
142     GtWinPrivate* priv = gt_win_get_instance_private(self);
143     GtkWidget* about_dlg = NULL;
144 
145     const char* authors[] = {"Vincent Szolnoky", NULL};
146     const char* contributors[] = {"Dimitrios Christidis", NULL};
147 
148     about_dlg = gtk_about_dialog_new();
149 
150     g_object_set(about_dlg,
151                  "version", GT_VERSION,
152                  "program-name", "GNOME Twitch",
153                  "authors", &authors,
154                  "license-type", GTK_LICENSE_GPL_3_0,
155                  "copyright", "Copyright © 2017 Vincent Szolnoky",
156                  "comments", _("Enjoy Twitch on your GNU/Linux desktop"),
157                  "logo-icon-name", "com.vinszent.GnomeTwitch",
158                  "website", "https://github.com/vinszent/gnome-twitch",
159                  "website-label", "GitHub",
160                  // Translators: Put your details here :)
161                  "translator-credits", _("translator-credits"),
162                  NULL);
163 
164     gtk_about_dialog_add_credit_section(GTK_ABOUT_DIALOG(about_dlg), _("Contributors"), contributors);
165     gtk_window_set_transient_for(GTK_WINDOW(about_dlg), GTK_WINDOW(self));
166 
167     gtk_dialog_run(GTK_DIALOG(about_dlg));
168     gtk_widget_destroy(about_dlg);
169 }
170 
171 static void
show_settings_cb(GSimpleAction * action,GVariant * par,gpointer udata)172 show_settings_cb(GSimpleAction* action,
173                  GVariant* par,
174                  gpointer udata)
175 {
176     GtWin* self = GT_WIN(udata);
177     GtWinPrivate* priv = gt_win_get_instance_private(self);
178 
179     if (!priv->settings_dlg)
180     {
181         priv->settings_dlg = gt_settings_dlg_new(self);
182         g_object_add_weak_pointer(G_OBJECT(priv->settings_dlg), (gpointer *) &priv->settings_dlg);
183     }
184 
185     if (par)
186     {
187         GEnumClass* eclass = g_type_class_ref(GT_TYPE_SETTINGS_DLG_VIEW);
188         GEnumValue* eval = g_enum_get_value_by_nick(eclass, g_variant_get_string(par, NULL));
189 
190         gt_settings_dlg_set_view(priv->settings_dlg, eval->value);
191 
192         g_simple_action_set_state(action, par);
193 
194         g_type_class_unref(eclass);
195     }
196 
197     gtk_window_present(GTK_WINDOW(priv->settings_dlg));
198 }
199 
200 static void
refresh_login_cb(GtkInfoBar * info_bar,gint res,gpointer udata)201 refresh_login_cb(GtkInfoBar* info_bar,
202                  gint res,
203                  gpointer udata)
204 {
205     GtWin* self = GT_WIN(udata);
206     GtWinPrivate* priv = gt_win_get_instance_private(self);
207 
208     switch (res)
209     {
210         case GTK_RESPONSE_YES:
211             gtk_window_present(GTK_WINDOW(gt_twitch_login_dlg_new(GTK_WINDOW(self))));
212             break;
213     }
214 }
215 
216 static void
show_error_dialogue_cb(GtkInfoBar * info_bar,gint res,gchar ** udata)217 show_error_dialogue_cb(GtkInfoBar* info_bar,
218     gint res, gchar** udata)
219 {
220     GtkBuilder* builder = gtk_builder_new_from_resource("/com/vinszent/GnomeTwitch/ui/gt-error-dlg.ui");
221     GtkWidget* dlg = GTK_WIDGET(gtk_builder_get_object(builder, "dlg"));
222     GtkTextView* details_text_view = GTK_TEXT_VIEW(gtk_builder_get_object(builder, "details_text_view"));
223     GtkLabel* sub_label = GTK_LABEL(gtk_builder_get_object(builder, "error_label"));
224     GtkWidget* close_button = GTK_WIDGET(gtk_builder_get_object(builder, "close_button"));
225     GtkWidget* report_button = GTK_WIDGET(gtk_builder_get_object(builder, "report_button"));
226     g_autofree gchar* info_text = NULL;
227 
228     if (res != GTK_RESPONSE_OK) goto cleanup;
229 
230     gtk_text_buffer_set_text(gtk_text_view_get_buffer(details_text_view),
231                              *(udata+1), -1);
232 
233     info_text = g_strdup_printf(_("<b>Error message:</b> %s"), *udata);
234 
235     gtk_label_set_label(sub_label, info_text);
236 
237     g_signal_connect_swapped(close_button, "clicked", G_CALLBACK(gtk_widget_destroy), dlg);
238 
239     //TODO: Hook up report button
240 
241     gtk_window_set_transient_for(GTK_WINDOW(dlg), GTK_WINDOW(GT_WIN_ACTIVE));
242     gtk_widget_show(dlg);
243 
244 cleanup:
245     g_strfreev(udata);
246 }
247 
248 
249 static void
show_info_bar(GtWin * self)250 show_info_bar(GtWin* self)
251 {
252     GtWinPrivate* priv = gt_win_get_instance_private(self);
253     QueuedInfoData* data = g_queue_pop_head(priv->info_queue);
254 
255     if (data)
256     {
257         if (data->cb)
258         {
259             if (data->cb == G_CALLBACK(show_error_dialogue_cb))
260             {
261                 gtk_widget_set_visible(priv->info_bar_ok_button, FALSE);
262                 gtk_widget_set_visible(priv->info_bar_yes_button, FALSE);
263                 gtk_widget_set_visible(priv->info_bar_no_button, FALSE);
264                 gtk_widget_set_visible(priv->info_bar_details_button, TRUE);
265                 gtk_widget_set_visible(priv->info_bar_close_button, TRUE);
266                 gtk_label_set_markup(GTK_LABEL(priv->info_label), data->msg);
267                 gtk_info_bar_set_message_type(GTK_INFO_BAR(priv->info_bar), GTK_MESSAGE_ERROR);
268 
269                 g_signal_connect(priv->info_bar, "response", G_CALLBACK(data->cb), data->udata);
270             }
271             else
272             {
273 
274                 g_signal_connect(priv->info_bar, "response", G_CALLBACK(data->cb), data->udata);
275 
276                 gtk_widget_set_visible(priv->info_bar_ok_button, FALSE);
277                 gtk_widget_set_visible(priv->info_bar_yes_button, TRUE);
278                 gtk_widget_set_visible(priv->info_bar_no_button, TRUE);
279                 gtk_widget_set_visible(priv->info_bar_details_button, FALSE);
280                 gtk_widget_set_visible(priv->info_bar_close_button, FALSE);
281                 gtk_label_set_markup(GTK_LABEL(priv->info_label), data->msg);
282                 gtk_info_bar_set_message_type(GTK_INFO_BAR(priv->info_bar), GTK_MESSAGE_QUESTION);
283             }
284         }
285         else
286         {
287             gtk_widget_set_visible(priv->info_bar_yes_button, FALSE);
288             gtk_widget_set_visible(priv->info_bar_no_button, FALSE);
289             gtk_widget_set_visible(priv->info_bar_ok_button, TRUE);
290             gtk_widget_set_visible(priv->info_bar_details_button, FALSE);
291             gtk_widget_set_visible(priv->info_bar_close_button, FALSE);
292             gtk_label_set_markup(GTK_LABEL(priv->info_label), data->msg);
293             gtk_info_bar_set_message_type(GTK_INFO_BAR(priv->info_bar), GTK_MESSAGE_INFO);
294         }
295 
296         priv->cur_info_data = data;
297 
298         gtk_revealer_set_reveal_child(GTK_REVEALER(priv->info_revealer), TRUE);
299     }
300     else
301     {
302         priv->cur_info_data = NULL;
303 
304         gtk_revealer_set_reveal_child(GTK_REVEALER(priv->info_revealer), FALSE);
305     }
306 }
307 
308 static void
close_info_bar_cb(GtkInfoBar * bar,gint res,gpointer udata)309 close_info_bar_cb(GtkInfoBar* bar,
310                   gint res,
311                   gpointer udata)
312 {
313     GtWin* self = GT_WIN(udata);
314     GtWinPrivate* priv = gt_win_get_instance_private(self);
315 
316     if (priv->cur_info_data)
317     {
318         if (priv->cur_info_data->cb)
319         {
320             g_signal_handlers_disconnect_by_func(priv->info_bar,
321                                                  priv->cur_info_data->cb,
322                                                  priv->cur_info_data->udata);
323         }
324 
325         g_free(priv->cur_info_data->msg);
326         g_free(priv->cur_info_data);
327     }
328 
329     show_info_bar(self);
330 }
331 
332 
333 static void
show_twitch_login_cb(GSimpleAction * action,GVariant * par,gpointer udata)334 show_twitch_login_cb(GSimpleAction* action,
335                      GVariant* par,
336                      gpointer udata)
337 {
338     GtWin* self = GT_WIN(udata);
339     GtWinPrivate* priv = gt_win_get_instance_private(self);
340 
341     if (gt_app_is_logged_in(main_app))
342     {
343         gt_win_ask_question(self, _("Already logged into Twitch, refresh login?"),
344                             G_CALLBACK(refresh_login_cb), self);
345     }
346     else
347     {
348         GtTwitchLoginDlg* dlg = gt_twitch_login_dlg_new(GTK_WINDOW(self));
349 
350         gtk_window_present(GTK_WINDOW(dlg));
351     }
352 }
353 
354 static void
show_channel_info_cb(GSimpleAction * action,GVariant * arg,gpointer udata)355 show_channel_info_cb(GSimpleAction* action,
356                      GVariant* arg,
357                      gpointer udata)
358 {
359     GtWin* self = GT_WIN(udata);
360     GtWinPrivate* priv = gt_win_get_instance_private(self);
361     GtTwitchChannelInfoDlg* dlg = gt_twitch_channel_info_dlg_new(GTK_WINDOW(self));
362     GtChannel* channel;
363     const gchar* name;
364 
365     g_object_get(self->player, "channel", &channel, NULL);
366 
367     name = gt_channel_get_name(channel);
368 
369     g_message("{GtWin} Showing channel info for '%s'", name);
370 
371     gtk_window_present(GTK_WINDOW(dlg));
372 
373     gt_twitch_channel_info_dlg_load_channel(dlg, name);
374 
375     g_object_unref(channel);
376 }
377 
378 static void
go_back_cb(GSimpleAction * action,GVariant * arg,gpointer udata)379 go_back_cb(GSimpleAction* action,
380     GVariant* arg, gpointer udata)
381 {
382     g_assert(GT_IS_WIN(udata));
383 
384     GtWin* self = GT_WIN(udata);
385     GtWinPrivate* priv = gt_win_get_instance_private(self);
386 
387     GtkWidget* current_view = gtk_stack_get_visible_child(GTK_STACK(priv->browse_stack));
388 
389     g_assert(GT_IS_CONTAINER_VIEW(current_view));
390 
391     gt_container_view_go_back(GT_CONTAINER_VIEW(current_view));
392 }
393 
394 static void
refresh_cb(GSimpleAction * action,GVariant * arg,gpointer udata)395 refresh_cb(GSimpleAction* action,
396     GVariant* arg, gpointer udata)
397 {
398     g_assert(GT_IS_WIN(udata));
399 
400     GtWin* self = GT_WIN(udata);
401     GtWinPrivate* priv = gt_win_get_instance_private(self);
402 
403     GtkWidget* current_view = gtk_stack_get_visible_child(GTK_STACK(priv->browse_stack));
404 
405     g_assert(GT_IS_CONTAINER_VIEW(current_view));
406 
407     gt_container_view_refresh(GT_CONTAINER_VIEW(current_view));
408 }
409 
410 static gboolean
key_press_cb(GtkWidget * widget,GdkEventKey * evt,gpointer udata)411 key_press_cb(GtkWidget* widget,
412              GdkEventKey* evt,
413              gpointer udata)
414 {
415     GtWin* self = GT_WIN(udata);
416     GtWinPrivate* priv = gt_win_get_instance_private(self);
417     GdkModifierType modifiers = gtk_accelerator_get_default_mod_mask();
418     GtkWidget* visible_child = gtk_stack_get_visible_child(GTK_STACK(priv->main_stack));
419     gboolean playing;
420     GAction *action = NULL;
421 
422     g_object_get(self->player, "playing", &playing, NULL);
423 
424     if (visible_child == GTK_WIDGET(self->player))
425     {
426         if (evt->keyval == GDK_KEY_Escape)
427         {
428             if (priv->fullscreen)
429                 g_object_set(self, "fullscreen", FALSE, NULL);
430             else
431             {
432                 action = g_action_map_lookup_action(G_ACTION_MAP(self), "close_player");
433                 g_action_activate(action, NULL);
434             }
435         }
436         else if (evt->keyval == GDK_KEY_f)
437         {
438             g_object_set(self, "fullscreen", !priv->fullscreen, NULL);
439         }
440     }
441     else
442     {
443         if (evt->keyval == GDK_KEY_Escape)
444             gt_browse_header_bar_stop_search(GT_BROWSE_HEADER_BAR(priv->browse_header_bar));
445         else if (evt->keyval == GDK_KEY_f && (evt->state & modifiers) == GDK_CONTROL_MASK)
446             gt_browse_header_bar_toggle_search(GT_BROWSE_HEADER_BAR(priv->browse_header_bar));
447         else
448         {
449             /* GtkWidget* view = gtk_stack_get_visible_child(GTK_STACK(priv->browse_stack)); */
450 
451             /* if (view == priv->channels_view) */
452             /*     gt_channels_view_handle_event(GT_CHANNELS_VIEW(priv->channels_view), (GdkEvent*) evt); */
453             /* else if (view == priv->games_view) */
454             /*     gt_games_view_handle_event(GT_GAMES_VIEW(priv->games_view), (GdkEvent*) evt); */
455             /* else if (view == priv->follows_view) */
456             /*     gt_follows_view_handle_event(GT_FOLLOWS_VIEW(priv->follows_view), (GdkEvent* )evt); */
457         }
458     }
459 
460     return FALSE;
461 }
462 
463 static gboolean
delete_cb(GtkWidget * widget,GdkEvent * evt,gpointer udata)464 delete_cb(GtkWidget* widget,
465           GdkEvent* evt,
466           gpointer udata)
467 {
468     GtWin* self = GT_WIN(udata);
469     GtWinPrivate* priv = gt_win_get_instance_private(self);
470     int width;
471     int height;
472 
473     gtk_window_get_size(GTK_WINDOW(self), &width, &height);
474 
475     g_settings_set_int(main_app->settings, "window-width", width);
476     g_settings_set_int(main_app->settings, "window-height", height);
477 
478     return FALSE;
479 }
480 
481 static void
close_channel_cb(GSimpleAction * action,GVariant * arg,gpointer udata)482 close_channel_cb(GSimpleAction* action,
483     GVariant* arg, gpointer udata)
484 {
485     RETURN_IF_FAIL(G_IS_SIMPLE_ACTION(action));
486     RETURN_IF_FAIL(arg == NULL);
487     RETURN_IF_FAIL(GT_IS_WIN(udata));
488 
489     GtWin* self = GT_WIN(udata);
490     GtWinPrivate* priv = gt_win_get_instance_private(self);
491 
492     gt_player_close_channel(GT_PLAYER(self->player));
493 
494     gtk_stack_set_visible_child(GTK_STACK(priv->main_stack),
495         priv->browse_stack);
496 }
497 
498 static void
close_player_cb(GSimpleAction * action,GVariant * arg,gpointer udata)499 close_player_cb(GSimpleAction* action,
500                 GVariant* arg,
501                 gpointer udata)
502 {
503     GtWin* self = GT_WIN(udata);
504     GtWinPrivate* priv = gt_win_get_instance_private(self);
505 
506     gt_player_close_channel(GT_PLAYER(self->player));
507 
508     gtk_stack_set_visible_child_name(GTK_STACK(priv->header_stack),
509                                      "browse");
510     gtk_stack_set_visible_child_name(GTK_STACK(priv->main_stack),
511                                      "browse");
512 }
513 
514 static void
show_vods_cb(GSimpleAction * action,GVariant * arg,gpointer udata)515 show_vods_cb(GSimpleAction* action,
516     GVariant* arg, gpointer udata)
517 {
518     RETURN_IF_FAIL(G_IS_SIMPLE_ACTION(action));
519     RETURN_IF_FAIL(GT_IS_WIN(udata));
520 
521     GtWin* self = GT_WIN(udata);
522     GtWinPrivate* priv = gt_win_get_instance_private(self);
523 
524     RETURN_IF_FAIL(self->open_channel);
525 
526     gtk_stack_set_visible_child(GTK_STACK(priv->channel_stack), priv->channel_vod_container);
527 }
528 
529 static gboolean
window_state_event_cb(GtkWidget * widget,GdkEventWindowState * evt,gpointer udata)530 window_state_event_cb(GtkWidget* widget,
531     GdkEventWindowState* evt, gpointer udata)
532 {
533     RETURN_VAL_IF_FAIL(GT_IS_WIN(widget), GDK_EVENT_PROPAGATE);
534     RETURN_VAL_IF_FAIL(udata == NULL, GDK_EVENT_PROPAGATE);
535 
536     GtWin* self = GT_WIN(widget);
537     GtWinPrivate* priv = gt_win_get_instance_private(self);
538 
539     priv->fullscreen = evt->new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
540 
541     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_FULLSCREEN]);
542 
543     return GDK_EVENT_PROPAGATE;
544 }
545 
546 static void
update_fullscreen(GtWin * self,gboolean fullscreen)547 update_fullscreen(GtWin* self, gboolean fullscreen)
548 {
549     if (fullscreen)
550         gtk_window_fullscreen(GTK_WINDOW(self));
551     else
552         gtk_window_unfullscreen(GTK_WINDOW(self));
553 }
554 
555 static GActionEntry win_actions[] =
556 {
557     {"refresh", refresh_cb, NULL, NULL, NULL},
558     {"go_back", go_back_cb, NULL, NULL, NULL},
559     {"show_about", show_about_cb, NULL, NULL, NULL},
560     {"show_settings", show_settings_cb, NULL, NULL},
561     {"show_settings_with_view", NULL, "s", "'general'", show_settings_cb},
562     {"show_twitch_login", show_twitch_login_cb, NULL, NULL, NULL},
563     {"show_channel_info", show_channel_info_cb, NULL, NULL, NULL},
564     {"close_channel", close_channel_cb, NULL, NULL, NULL},
565     {"show_vods", show_vods_cb, NULL, NULL, NULL},
566 };
567 
568 static void
finalize(GObject * object)569 finalize(GObject* object)
570 {
571     GtWin* self = (GtWin*) object;
572     GtWinPrivate* priv = gt_win_get_instance_private(self);
573 
574     G_OBJECT_CLASS(gt_win_parent_class)->finalize(object);
575 
576     g_message("{GtWin} Finalise");
577 }
578 
579 static void
get_property(GObject * obj,guint prop,GValue * val,GParamSpec * pspec)580 get_property (GObject*    obj,
581               guint       prop,
582               GValue*     val,
583               GParamSpec* pspec)
584 {
585     GtWin* self = GT_WIN(obj);
586     GtWinPrivate* priv = gt_win_get_instance_private(self);
587 
588     switch (prop)
589     {
590         case PROP_FULLSCREEN:
591             g_value_set_boolean(val, priv->fullscreen);
592             break;
593         case PROP_OPEN_CHANNEL:
594             g_value_set_object(val, self->open_channel);
595             break;
596         default:
597             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
598     }
599 }
600 
601 static void
set_property(GObject * obj,guint prop,const GValue * val,GParamSpec * pspec)602 set_property(GObject*      obj,
603              guint         prop,
604              const GValue* val,
605              GParamSpec*   pspec)
606 {
607     GtWin* self = GT_WIN(obj);
608 
609     switch (prop)
610     {
611         case PROP_FULLSCREEN:
612             update_fullscreen(self, g_value_get_boolean(val));
613             break;
614         case PROP_OPEN_CHANNEL:
615             g_clear_object(&self->open_channel);
616             self->open_channel = g_value_dup_object(val);
617             break;
618         default:
619             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
620     }
621 }
622 
623 static void
constructed(GObject * obj)624 constructed(GObject* obj)
625 {
626     GtWin* self = GT_WIN(obj);
627     GtWinPrivate* priv = gt_win_get_instance_private(self);
628 
629     g_signal_connect(priv->browse_stack, "notify::visible-child",
630         G_CALLBACK(container_view_changed_cb), self);
631 
632     container_view_changed_cb(NULL, NULL, self);
633 
634     G_OBJECT_CLASS(gt_win_parent_class)->constructed(obj);
635 }
636 
637 static void
gt_win_class_init(GtWinClass * klass)638 gt_win_class_init(GtWinClass* klass)
639 {
640     GObjectClass* object_class = G_OBJECT_CLASS(klass);
641 
642     object_class->finalize = finalize;
643     object_class->get_property = get_property;
644     object_class->set_property = set_property;
645     object_class->constructed = constructed;
646 
647     props[PROP_FULLSCREEN] = g_param_spec_boolean("fullscreen", "Fullscreen", "Whether window is fullscreen",
648         FALSE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY);
649 
650     props[PROP_OPEN_CHANNEL] = g_param_spec_object("open-channel", "Open channel", "Currenly open channel",
651         GT_TYPE_CHANNEL, G_PARAM_READWRITE);
652 
653     g_object_class_install_properties(object_class, NUM_PROPS, props);
654 
655     gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS(klass),
656                                                 "/com/vinszent/GnomeTwitch/ui/gt-win.ui");
657     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, main_stack);
658     gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(klass), GtWin, player);
659     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, header_stack);
660     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, browse_header_bar);
661     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, browse_stack);
662     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, browse_stack_switcher);
663     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, info_revealer);
664     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, info_label);
665     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, info_bar);
666     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, info_bar_yes_button);
667     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, info_bar_no_button);
668     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, info_bar_ok_button);
669     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, info_bar_details_button);
670     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, info_bar_close_button);
671     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, channel_stack);
672     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, channel_vod_container);
673     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtWin, channel_header_bar);
674 
675     GT_TYPE_PLAYER; // Hack to load GtPlayer into the symbols table
676     GT_TYPE_BROWSE_HEADER_BAR;
677     GT_TYPE_CHANNEL_CONTAINER_VIEW;
678     GT_TYPE_FOLLOWED_CONTAINER_VIEW;
679     GT_TYPE_GAME_CONTAINER_VIEW;
680     GT_TYPE_CHANNEL_VOD_CONTAINER;
681     GT_TYPE_CHANNEL_HEADER_BAR;
682     GT_TYPE_CHAT;
683 }
684 
685 static void
gt_win_init(GtWin * self)686 gt_win_init(GtWin* self)
687 {
688     GtWinPrivate* priv = gt_win_get_instance_private(self);
689     GPropertyAction* action;
690 
691     gtk_window_set_application(GTK_WINDOW(self), GTK_APPLICATION(main_app));
692 
693     gtk_widget_init_template(GTK_WIDGET(self));
694     /* NOTE: Win will already be realized after we init the template */
695     gtk_widget_realize(priv->channel_header_bar);
696 
697     priv->cur_info_data = NULL;
698     priv->info_queue = g_queue_new();
699 
700     gtk_window_set_default_size(GTK_WINDOW(self),
701                                 g_settings_get_int(main_app->settings, "window-width"),
702                                 g_settings_get_int(main_app->settings, "window-height"));
703 
704     gtk_window_set_default_icon_name("com.vinszent.GnomeTwitch");
705 
706     gtk_window_set_icon_name(GTK_WINDOW(self), "com.vinszent.GnomeTwitch");
707 
708     GdkScreen* screen = gdk_screen_get_default();
709     GtkCssProvider* css = gtk_css_provider_new();
710     gtk_css_provider_load_from_resource(css, "/com/vinszent/GnomeTwitch/com.vinszent.GnomeTwitch.style.css");
711     gtk_style_context_add_provider_for_screen(screen, GTK_STYLE_PROVIDER(css),
712                                               GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
713 
714     g_signal_connect_after(self, "key-press-event", G_CALLBACK(key_press_cb), self);
715     g_signal_connect(self, "delete-event", G_CALLBACK(delete_cb), self);
716     g_signal_connect_after(priv->info_bar, "response", G_CALLBACK(close_info_bar_cb), self);
717     g_signal_connect(self, "window-state-event", G_CALLBACK(window_state_event_cb), NULL);
718 
719     g_action_map_add_action_entries(G_ACTION_MAP(self),
720                                     win_actions,
721                                     G_N_ELEMENTS(win_actions),
722                                     self);
723 
724     action = g_property_action_new("toggle_fullscreen", self, "fullscreen");
725     g_action_map_add_action(G_ACTION_MAP(self),
726                             G_ACTION(action));
727     g_object_unref(action);
728 
729     GtkWindowGroup* window_group = gtk_window_group_new();
730     gtk_window_group_add_window(window_group, GTK_WINDOW(self));
731     g_object_unref(window_group);
732 }
733 
734 void
gt_win_open_channel(GtWin * self,GtChannel * chan)735 gt_win_open_channel(GtWin* self, GtChannel* chan)
736 {
737     g_assert(GT_IS_WIN(self));
738     g_assert(GT_IS_CHANNEL(chan));
739 
740     GtWinPrivate* priv = gt_win_get_instance_private(self);
741 
742     g_object_set(self, "open-channel", chan, NULL);
743 
744     gt_player_open_channel(GT_PLAYER(self->player), chan);
745 
746     g_object_set(G_OBJECT(priv->channel_vod_container), "channel-id", gt_channel_get_id(chan), NULL);
747 
748     gt_item_container_refresh(GT_ITEM_CONTAINER(priv->channel_vod_container));
749 
750     gtk_stack_set_visible_child(GTK_STACK(priv->channel_stack), GTK_WIDGET(self->player));
751     gtk_stack_set_visible_child(GTK_STACK(priv->main_stack), priv->channel_stack);
752 }
753 
754 void
gt_win_play_vod(GtWin * self,GtVOD * vod)755 gt_win_play_vod(GtWin* self, GtVOD* vod)
756 {
757     RETURN_IF_FAIL(GT_IS_WIN(self));
758     RETURN_IF_FAIL(GT_IS_VOD(vod));
759 
760     GtWinPrivate* priv = gt_win_get_instance_private(self);
761 
762     gt_player_open_vod(self->player, vod);
763     gtk_stack_set_visible_child(GTK_STACK(priv->channel_stack), GTK_WIDGET(self->player));
764 }
765 
766 gboolean
gt_win_is_fullscreen(GtWin * self)767 gt_win_is_fullscreen(GtWin* self)
768 {
769     GtWinPrivate* priv = gt_win_get_instance_private(self);
770 
771     return priv->fullscreen;
772 }
773 
774 void
gt_win_toggle_fullscreen(GtWin * self)775 gt_win_toggle_fullscreen(GtWin* self)
776 {
777     GtWinPrivate* priv = gt_win_get_instance_private(self);
778 
779     g_object_set(self, "fullscreen", !priv->fullscreen, NULL);
780 }
781 
782 void
gt_win_show_info_message(GtWin * self,const gchar * msg_fmt,...)783 gt_win_show_info_message(GtWin* self, const gchar* msg_fmt, ...)
784 {
785     RETURN_IF_FAIL(GT_IS_WIN(self));
786 
787     GtWinPrivate* priv = gt_win_get_instance_private(self);
788     QueuedInfoData* data = g_new0(QueuedInfoData, 1);
789     va_list fmt_list;
790 
791     va_start(fmt_list, msg_fmt);
792     data->msg = g_strdup_vprintf(msg_fmt, fmt_list);
793     va_end(fmt_list);
794 
795     g_queue_push_tail(priv->info_queue, data);
796 
797     if (!gtk_revealer_get_reveal_child(GTK_REVEALER(priv->info_revealer)))
798         show_info_bar(self);
799 }
800 
801 void
gt_win_show_error_message(GtWin * self,const gchar * secondary,const gchar * details_fmt,...)802 gt_win_show_error_message(GtWin* self, const gchar* secondary, const gchar* details_fmt, ...)
803 {
804     g_assert(GT_IS_WIN(self));
805 
806     gchar** udata = g_malloc(sizeof(gchar*)*3);
807     // Translators: Please keep the markup tags
808     gchar* msg = g_strdup_printf(_("<b>Something went wrong:</b> %s."), secondary);
809     va_list details_list;
810 
811     *udata = g_strdup(secondary);
812 
813     va_start(details_list, details_fmt);
814     *(udata+1) = g_strdup_vprintf(details_fmt, details_list);
815     va_end(details_list);
816     *(udata+2) = NULL;
817 
818     gt_win_ask_question(self, msg, G_CALLBACK(show_error_dialogue_cb), udata);
819 }
820 
821 void
gt_win_show_error_dialogue(GtWin * self,const gchar * secondary,const gchar * details_fmt,...)822 gt_win_show_error_dialogue(GtWin* self, const gchar* secondary, const gchar* details_fmt, ...)
823 {
824     g_assert(GT_IS_WIN(self));
825 
826     gchar** udata = g_malloc(sizeof(gchar*)*3);
827     va_list details_list;
828 
829     *udata = g_strdup(secondary);
830 
831     va_start(details_list, details_fmt);
832     *(udata+1) = g_strdup_vprintf(details_fmt, details_list);
833     va_end(details_list);
834     *(udata+2) = NULL;
835 
836     show_error_dialogue_cb(NULL, GTK_RESPONSE_OK, udata);
837 }
838 
839 void
gt_win_ask_question(GtWin * self,const gchar * msg,GCallback cb,gpointer udata)840 gt_win_ask_question(GtWin* self, const gchar* msg, GCallback cb, gpointer udata)
841 {
842     GtWinPrivate* priv = gt_win_get_instance_private(self);
843     QueuedInfoData* data = g_new(QueuedInfoData, 1);
844 
845     data->msg = g_strdup(msg);
846     data->cb = cb;
847     data->udata = udata;
848 
849     g_queue_push_tail(priv->info_queue, data);
850 
851     if (!gtk_revealer_get_reveal_child(GTK_REVEALER(priv->info_revealer)))
852         show_info_bar(self);
853 }
854 
855 void
gt_win_inhibit_screensaver(GtWin * self)856 gt_win_inhibit_screensaver(GtWin* self)
857 {
858     RETURN_IF_FAIL(GT_IS_WIN(self));
859 
860     GtWinPrivate* priv = gt_win_get_instance_private(self);
861 
862     if (priv->inhibit_cookie > 0)
863     {
864         WARNING("Already inhibiting screensaver");
865         return;
866     }
867 
868     priv->inhibit_cookie = gtk_application_inhibit(GTK_APPLICATION(main_app),
869         GTK_WINDOW(self), GTK_APPLICATION_INHIBIT_IDLE, "Playing a stream");
870 
871     if (priv->inhibit_cookie == 0)
872     {
873         INFO("Unable to inhibit screensaver via application, attempting via DBus");
874 
875         /* TODO: Use the async versions of the dbus funcs */
876         g_autoptr(GError) err = NULL;
877         g_autoptr(GDBusProxy) proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, NULL,
878             "org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", NULL, &err);
879         g_autoptr(GVariant) ret = NULL;
880 
881         if (err)
882         {
883             WARNING("Unable to create DBus proxy to inhibit screensaver because: %s", err->message);
884             return;
885         }
886 
887         ret = g_dbus_proxy_call_sync(proxy, "Inhibit",
888             g_variant_new("(ss)", "GNOME Twitch", "Playing a stream"),
889             G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
890 
891         if (err)
892         {
893             WARNING("Unable to call inhibit function on DBus proxy because: %s", err->message);
894             return;
895         }
896 
897         g_variant_get(ret, "(u)", &priv->inhibit_cookie);
898     }
899 
900     if (priv->inhibit_cookie > 0)
901         INFO("Successfully inhibited screensaver");
902 }
903 
904 void
gt_win_uninhibit_screensaver(GtWin * self)905 gt_win_uninhibit_screensaver(GtWin* self)
906 {
907     RETURN_IF_FAIL(GT_IS_WIN(self));
908 
909     GtWinPrivate* priv = gt_win_get_instance_private(self);
910 
911     if (priv->inhibit_cookie == 0)
912     {
913         WARNING("Not currently inhibiting screensaver");
914         return;
915     }
916 
917     if (gtk_application_is_inhibited(GTK_APPLICATION(main_app), GTK_APPLICATION_INHIBIT_IDLE))
918         gtk_application_uninhibit(GTK_APPLICATION(main_app), priv->inhibit_cookie);
919 
920     /* NOTE: If we are still inhibited, try uninhibiting via DBus */
921     if (gtk_application_is_inhibited(GTK_APPLICATION(main_app), GTK_APPLICATION_INHIBIT_IDLE))
922     {
923         g_autoptr(GError) err = NULL;
924         g_autoptr(GDBusProxy) proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, NULL,
925             "org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", NULL, &err);
926         g_autoptr(GVariant) ret = NULL;
927 
928         if (err)
929         {
930             WARNING("Unable to create DBus proxy to uninhibit screensaver because: %s", err->message);
931             return;
932         }
933 
934         ret = g_dbus_proxy_call_sync(proxy, "UnInhibit",
935             g_variant_new("(u)", priv->inhibit_cookie),
936             G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
937 
938         if (err)
939         {
940             WARNING("Unable to call uninhibit function on DBus proxy because: %s", err->message);
941             return;
942         }
943     }
944 
945     INFO("Successfully uninhibited screensaver");
946 
947     priv->inhibit_cookie = 0;
948 }
949