├── .gitignore
├── groover.gresources.xml
├── groover.desktop
├── .ycm_extra_conf.py
├── menus.xml
├── README.md
├── Makefile
└── groover.c
/.gitignore:
--------------------------------------------------------------------------------
1 | *.o
2 | groover
3 | *.py[co]
4 | .*.sw[op]
5 |
--------------------------------------------------------------------------------
/groover.gresources.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | menus.xml
5 |
6 |
7 |
9 |
--------------------------------------------------------------------------------
/groover.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Hidden=false
3 | Name=Groover
4 | Exec=groover
5 | TryExec=groover
6 | Icon=gnome-music
7 | Comment=Control the Groove Basin music player daemon
8 | Type=Application
9 | NoDisplay=false
10 | StartupNotify=true
11 | StartupWMClass=Groover
12 | Categories=GNOME;GTK;Multimedia;
13 | Terminal=false
14 |
--------------------------------------------------------------------------------
/.ycm_extra_conf.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # vim:fenc=utf-8
4 | #
5 | # Copyright © 2014 Adrian Perez
6 | #
7 | # Distributed under terms of the MIT license.
8 |
9 | from subprocess import check_output
10 | from shlex import split as sh_split
11 |
12 | def FlagsForFile(path, **kwarg):
13 | flags = sh_split(check_output(["make", "print-flags"]))
14 | flags.append("-Qunused-arguments")
15 | return { 'flags': flags, 'do_cache': True }
16 |
17 |
--------------------------------------------------------------------------------
/menus.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
22 |
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Groover
2 |
3 | Groover is a simple remote for the [Groove Basin](http://groovebasin.com)
4 | music player daemon. It uses GTK+ and WebKitGTK+ to provide a window where
5 | Groove Basin's standard web interface is loaded — honestly I do not see the
6 | whole point of re-creating the UI from scratch, so the idea is to keep
7 | Groover simple, and focus on desktop integration instead.
8 |
9 |
10 | ## Features
11 |
12 | - Shows the Groove Basin UI as a standalone application (and its own icon,
13 | and so on), without the need to launch a complete web browser to access
14 | it. This is not very different from the “web application mode” of Web;
15 | but does not require it to be installed.
16 |
17 |
18 | ## (Planned) Features
19 |
20 | - Simple “Now Playing” screen mode which uses only GTK+ widgets, without
21 | requiring WebKitGTK+ to load Groove Basin's web UI.
22 | - Support acting as a command-line remote for Groove Basin.
23 | - MPRIS2 provider, so other applications can control Groove Basin via a
24 | compatible client.
25 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | PREFIX ?= /usr/local
3 | CFLAGS += -Wall -std=gnu99
4 | CPPFLAGS += -DG_DISABLE_DEPRECATED \
5 | -DPREFIX=\"$(PREFIX)\"
6 |
7 | # Usage:
8 | # $(eval $(call PKG, name, module1 [module2 [... moduleN]]))
9 | #
10 | # Creates variables:
11 | # PKG_name_MODULES
12 | # PKG_name_CFLAGS
13 | # PKG_name_LDLIBS
14 | define PKG
15 | PKG_$1_MODULES := $2
16 | PKG_$1_CFLAGS := $$(shell pkg-config $$(PKG_$1_MODULES) --cflags)
17 | PKG_$1_LDLIBS := $$(shell pkg-config $$(PKG_$1_MODULES) --libs)
18 | endef
19 |
20 | $(eval $(call PKG,WEBKIT,webkit2gtk-3.0 gtk+-3.0))
21 | $(eval $(call PKG,WEBKIT_EXTENSION,webkit2gtk-web-extension-3.0))
22 |
23 | all: groover groover.desktop
24 |
25 | groover: CFLAGS += $(PKG_WEBKIT_CFLAGS)
26 | groover: LDFLAGS += $(PKG_WEBKIT_LDLIBS)
27 | groover: groover.o groover.gresources.o
28 |
29 | #%: %.o
30 | # $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
31 |
32 | groover.gresources.o: menus.xml
33 |
34 | %.gresources.c: %.gresources.xml
35 | glib-compile-resources --generate-source --target=$@ $<
36 |
37 | %.gresources.h: %.gresources.xml
38 | glib-compile-resources --generate-header --target=$@ $<
39 |
40 | clean:
41 | $(RM) groover groover.o
42 |
43 | install: all
44 | install -m 755 -d $(DESTDIR)$(PREFIX)/bin
45 | install -m 755 -t $(DESTDIR)$(PREFIX)/bin groover
46 | install -m 755 -d $(DESTDIR)$(PREFIX)/share/applications
47 | install -m 644 -t $(DESTDIR)$(PREFIX)/share/applications groover.desktop
48 |
49 | ifeq ($(origin TAG),command line)
50 | VERSION := $(TAG)
51 | else
52 | VERSION := $(shell git tag 2> /dev/null | tail -1)
53 | endif
54 |
55 | dist:
56 | ifeq ($(strip $(VERSION)),)
57 | @echo "ERROR: Either Git is not installed, or no tags were found"
58 | else
59 | git archive --prefix=groover-$(VERSION)/ $(VERSION) | xz -c > groover-$(VERSION).tar.xz
60 | endif
61 |
62 | print-flags:
63 | @echo "$(PKG_WEBKIT_CFLAGS) $(PKG_WEBKIT_EXTENSION) $(CPPFLAGS)"
64 |
65 | .PHONY: clean install dist print-flags
66 |
67 |
--------------------------------------------------------------------------------
/groover.c:
--------------------------------------------------------------------------------
1 | /*
2 | * groover.c
3 | * Copyright (C) 2014 Adrian Perez
4 | *
5 | * Distributed under terms of the MIT license.
6 | */
7 |
8 | #include
9 | #include
10 |
11 | #ifndef WEB_EXTENSIONS_DIRECTORY
12 | # ifndef PREFIX
13 | # error Either WEB_EXTENSIONS_DIRECTORY or PREFIX must be defined
14 | # endif /* !PREFIX */
15 | # define WEB_EXTENSIONS_DIRECTORY (PREFIX "/lib/groover")
16 | #endif /* !WEB_EXTENSIONS_DIRECTORY */
17 |
18 | #define GROOVER_GRESOURCE(name) ("/org/perezdecastro/groover/" name)
19 |
20 |
21 | static void
22 | add_transport_buttons (GtkContainer *container)
23 | {
24 | const struct {
25 | const gchar *icon;
26 | const gchar *action;
27 | } button_map[] = {
28 | { "media-skip-backward-symbolic", "app.previous-song" },
29 | { "media-playback-start-symbolic", "app.toggle-play" },
30 | { "media-skip-forward-symbolic", "app.next-song" },
31 | };
32 |
33 | for (guint i = 0; i < G_N_ELEMENTS (button_map); i++) {
34 | GtkWidget *button = gtk_button_new_from_icon_name (button_map[i].icon,
35 | GTK_ICON_SIZE_BUTTON);
36 | gtk_button_set_focus_on_click (GTK_BUTTON (button), FALSE);
37 | gtk_style_context_add_class (gtk_widget_get_style_context (button), "image-button");
38 | gtk_style_context_add_class (gtk_widget_get_style_context (button), "linked");
39 | gtk_actionable_set_action_name (GTK_ACTIONABLE (button),
40 | button_map[i].action);
41 | gtk_container_add (container, button);
42 | }
43 |
44 | gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (container)),
45 | "raised");
46 | gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (container)),
47 | "linked");
48 | }
49 |
50 |
51 | static void
52 | application_window_setup_header_bar (GtkWindow *window)
53 | {
54 | GtkWidget *header = gtk_header_bar_new ();
55 | gtk_header_bar_set_show_close_button (GTK_HEADER_BAR (header), TRUE);
56 | gtk_header_bar_set_has_subtitle (GTK_HEADER_BAR (header), TRUE);
57 | gtk_header_bar_set_title (GTK_HEADER_BAR (header), "Groover");
58 |
59 | gtk_window_set_titlebar (window, header);
60 |
61 | GtkWidget *hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
62 | add_transport_buttons (GTK_CONTAINER (hbox));
63 | gtk_header_bar_pack_start (GTK_HEADER_BAR (header), hbox);
64 | }
65 |
66 |
67 | static GtkWidget *
68 | application_window_new (GtkApplication *application)
69 | {
70 | GtkWidget *window = gtk_application_window_new (application);
71 | gtk_window_set_title (GTK_WINDOW (window), "Groover");
72 | gtk_window_set_has_resize_grip (GTK_WINDOW (window), TRUE);
73 | // TODO: Save and restore window sizes
74 | gtk_widget_set_size_request (window, 900, 680);
75 | application_window_setup_header_bar (GTK_WINDOW (window));
76 |
77 | GtkWidget *web_view = webkit_web_view_new ();
78 | // TODO: Save the server URL in a setting
79 | webkit_web_view_load_uri (WEBKIT_WEB_VIEW (web_view),
80 | "http://127.0.0.1:16242");
81 |
82 | gtk_container_add (GTK_CONTAINER (window), web_view);
83 | return window;
84 | }
85 |
86 |
87 | static WebKitWebView *
88 | application_find_web_view (GtkApplication *application)
89 | {
90 | GtkWindow *window = gtk_application_get_active_window (application);
91 | return WEBKIT_WEB_VIEW (gtk_bin_get_child (GTK_BIN (window)));
92 | }
93 |
94 |
95 | static void
96 | run_javascript_finished_discard_result (GObject *object,
97 | GAsyncResult *result,
98 | gpointer userdata)
99 | {
100 | WebKitJavascriptResult *js_result =
101 | webkit_web_view_run_javascript_finish (WEBKIT_WEB_VIEW (object),
102 | result,
103 | NULL);
104 | webkit_javascript_result_unref (js_result);
105 | }
106 |
107 |
108 | static void
109 | prev_song_action_activated (GSimpleAction *action,
110 | GVariant *parameter,
111 | gpointer userdata)
112 | {
113 | WebKitWebView *web_view =
114 | application_find_web_view (GTK_APPLICATION (userdata));
115 | webkit_web_view_run_javascript (web_view,
116 | "_debug_player.prev()",
117 | NULL,
118 | run_javascript_finished_discard_result,
119 | NULL);
120 | }
121 |
122 |
123 | static void
124 | next_song_action_activated (GSimpleAction *action,
125 | GVariant *parameter,
126 | gpointer userdata)
127 | {
128 | WebKitWebView *web_view =
129 | application_find_web_view (GTK_APPLICATION (userdata));
130 | webkit_web_view_run_javascript (web_view,
131 | "_debug_player.next()",
132 | NULL,
133 | run_javascript_finished_discard_result,
134 | NULL);
135 | }
136 |
137 |
138 |
139 | static void
140 | toggle_play_action_activated (GSimpleAction *action,
141 | GVariant *parameter,
142 | gpointer userdata)
143 | {
144 | WebKitWebView *web_view =
145 | application_find_web_view (GTK_APPLICATION (userdata));
146 | webkit_web_view_run_javascript (web_view,
147 | "_debug_player.isPlaying"
148 | " ? _debug_player.pause()"
149 | " : _debug_player.play()",
150 | NULL,
151 | run_javascript_finished_discard_result,
152 | NULL);
153 | }
154 |
155 |
156 | static void
157 | about_action_activated (GSimpleAction *action,
158 | GVariant *parameter,
159 | gpointer userdata)
160 | {
161 | static const gchar *authors[] = {
162 | "Adrián Pérez de Castro",
163 | NULL,
164 | };
165 |
166 | gtk_show_about_dialog (NULL,
167 | "authors", authors,
168 | "logo-icon-name", "gnome-music",
169 | "license-type", GTK_LICENSE_MIT_X11,
170 | "comments", "A simple Groove Basin remote using its web-based UI",
171 | "website", "https://github.com/aperezdc/groover",
172 | NULL);
173 | }
174 |
175 |
176 | static void
177 | quit_action_activated (GSimpleAction *action,
178 | GVariant *parameter,
179 | gpointer userdata)
180 | {
181 | g_application_quit (G_APPLICATION (userdata));
182 | }
183 |
184 |
185 | static const GActionEntry app_actions[] = {
186 | { "next-song", next_song_action_activated, NULL, NULL, NULL },
187 | { "previous-song", prev_song_action_activated, NULL, NULL, NULL },
188 | { "toggle-play", toggle_play_action_activated, NULL, NULL, NULL },
189 | { "about", about_action_activated, NULL, NULL, NULL },
190 | { "quit", quit_action_activated, NULL, NULL, NULL },
191 | };
192 |
193 |
194 | static void
195 | application_started (GtkApplication *application)
196 | {
197 | WebKitWebContext *context = webkit_web_context_get_default ();
198 | webkit_web_context_set_process_model (context,
199 | WEBKIT_PROCESS_MODEL_SHARED_SECONDARY_PROCESS);
200 | webkit_web_context_set_web_extensions_directory (
201 | context, WEB_EXTENSIONS_DIRECTORY);
202 |
203 | gtk_window_set_default_icon_name ("gnome-music");
204 | g_object_set (gtk_settings_get_default (),
205 | "gtk-application-prefer-dark-theme",
206 | TRUE, NULL);
207 |
208 | g_action_map_add_action_entries (G_ACTION_MAP (application), app_actions,
209 | G_N_ELEMENTS (app_actions), application);
210 |
211 | GtkBuilder *builder = gtk_builder_new_from_resource (GROOVER_GRESOURCE ("menus.xml"));
212 | gtk_application_set_app_menu (application,
213 | G_MENU_MODEL (gtk_builder_get_object (builder, "app-menu")));
214 | g_object_unref (builder);
215 |
216 | const struct {
217 | const gchar *action;
218 | const gchar *accel;
219 | GVariant *param;
220 | } accel_map[] = {
221 | { "app.toggle-play", "m", NULL },
222 | { "app.previous-song", "comma", NULL },
223 | { "app.next-song", "period", NULL },
224 | };
225 |
226 | for (guint i = 0; i < G_N_ELEMENTS (accel_map); i++) {
227 | gtk_application_add_accelerator (application,
228 | accel_map[i].accel,
229 | accel_map[i].action,
230 | accel_map[i].param);
231 | }
232 | }
233 |
234 |
235 | static void
236 | application_activated (GtkApplication *application)
237 | {
238 | static GtkWidget *main_window = NULL;
239 |
240 | if (!main_window)
241 | main_window = application_window_new (application);
242 | gtk_widget_show_all (main_window);
243 | gtk_window_present (GTK_WINDOW (main_window));
244 | }
245 |
246 |
247 | static const gchar *
248 | get_application_id (void)
249 | {
250 | static const gchar *app_id = NULL;
251 | if (!app_id) {
252 | if (!(app_id = g_getenv ("GROOVER_APPLICATION_ID")))
253 | app_id = "org.perezdecastro.groover";
254 | }
255 | return app_id;
256 | }
257 |
258 |
259 | int
260 | main (int argc, char **argv)
261 | {
262 | GtkApplication *application =
263 | gtk_application_new (get_application_id (),
264 | G_APPLICATION_FLAGS_NONE);
265 |
266 | g_signal_connect (G_OBJECT (application), "startup",
267 | G_CALLBACK (application_started), NULL);
268 | g_signal_connect (G_OBJECT (application), "activate",
269 | G_CALLBACK (application_activated), NULL);
270 |
271 | gint status = g_application_run (G_APPLICATION (application), argc, argv);
272 | g_object_unref (application);
273 | return status;
274 | }
275 |
276 |
--------------------------------------------------------------------------------