diff --git a/aux/flatpak/space.arimelody.LiveSchedule.json b/aux/flatpak/space.arimelody.LiveSchedule.json new file mode 100644 index 0000000..aad748e --- /dev/null +++ b/aux/flatpak/space.arimelody.LiveSchedule.json @@ -0,0 +1,38 @@ +{ + "id" : "space.arimelody.LiveSchedule", + "runtime" : "org.gnome.Platform", + "runtime-version" : "48", + "sdk" : "org.gnome.Sdk", + "command" : "liveschedule", + "finish-args" : [ + "--share=network", + "--share=ipc", + "--socket=fallback-x11", + "--device=dri", + "--socket=wayland" + ], + "cleanup" : [ + "/include", + "/lib/pkgconfig", + "/man", + "/share/doc", + "/share/gtk-doc", + "/share/man", + "/share/pkgconfig", + "*.la", + "*.a" + ], + "modules" : [ + { + "name" : "liveschedule", + "builddir" : true, + "buildsystem" : "meson", + "sources" : [ + { + "type" : "git", + "url" : "file://var/run/media/julia/Projects/Live Schedule" + } + ] + } + ] +} diff --git a/main.js b/main.js index 151bc4e..2c034a2 100644 --- a/main.js +++ b/main.js @@ -114,12 +114,68 @@ function altText() { alert(altText); } -function copyClipboard() { - // TODO: screen capture API: https://developer.mozilla.org/en-US/docs/Web/API/Screen_Capture_API -} +if ('webkit' in window && 'messageHandlers' in window.webkit) { + let _promiseId = 0; + let _promises = new Map(); -function saveImage() { - // TODO: screen capture API: https://developer.mozilla.org/en-US/docs/Web/API/Screen_Capture_API + window.webkit.messageHandlers._nativePromiseResolved.addEventListener('message', (event) => { + const [id] = event.detail; + const [resolve,] = _promises.get(id) ?? []; + if (!resolve) return; + _promises.delete(id); + resolve(); + }); + + window.webkit.messageHandlers._nativePromiseRejected.addEventListener('message', (event) => { + const [id] = event.detail; + const [,reject] = _promises.get(id) ?? []; + if (!reject) return; + _promises.delete(id); + reject(); + }); + + function copyClipboard() { + return new Promise((resolve, reject) => { + const thisPromiseId = ++promiseId; + + _promises.set(thisPromiseId, [resolve, reject]); + + const schedule = document.getElementById('schedule'); + const boundingRect = getBoundingClientRect(); + + window.webkit.messageHandlers.copySnapshot.postMessage([ + thisPromiseId, + 'clipboard', + boundingRect.x, + boundingRect.y, + boundingRect.width, + boundingRect.height, + ]); + }); + } + + function saveImage() { + return new Promise((resolve, reject) => { + const thisPromiseId = ++promiseId; + + _promises.set(thisPromiseId, [resolve, reject]); + + const schedule = document.getElementById('schedule'); + const boundingRect = getBoundingClientRect(); + + window.webkit.messageHandlers.copySnapshot.postMessage([ + thisPromiseId, + 'file', + boundingRect.x, + boundingRect.y, + boundingRect.width, + boundingRect.height, + ]); + }); + } + + document.getElementById('clipboard').removeAttribute('disabled'); + document.getElementById('save-image').removeAttribute('disabled'); } function exportJSON() { diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..df28f8a --- /dev/null +++ b/meson.build @@ -0,0 +1,6 @@ +project('schedule', 'c', + version: '0.1.0', + meson_version: '>= 1.0.0', + default_options: [ 'warning_level=2', 'werror=false', 'c_std=gnu11', ]) + +subdir('native') diff --git a/native/data/icons/hicolor/scalable/apps/space.arimelody.Schedule.svg b/native/data/icons/hicolor/scalable/apps/space.arimelody.Schedule.svg new file mode 100644 index 0000000..a74c4df --- /dev/null +++ b/native/data/icons/hicolor/scalable/apps/space.arimelody.Schedule.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + application-x-executable + + + + + + + + + + + + + + + + diff --git a/native/data/icons/hicolor/symbolic/apps/space.arimelody.Schedule-symbolic.svg b/native/data/icons/hicolor/symbolic/apps/space.arimelody.Schedule-symbolic.svg new file mode 100644 index 0000000..0444828 --- /dev/null +++ b/native/data/icons/hicolor/symbolic/apps/space.arimelody.Schedule-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/native/data/icons/meson.build b/native/data/icons/meson.build new file mode 100644 index 0000000..18788cd --- /dev/null +++ b/native/data/icons/meson.build @@ -0,0 +1,13 @@ +application_id = 'space.arimelody.Schedule' + +scalable_dir = 'hicolor' / 'scalable' / 'apps' +install_data( + scalable_dir / ('@0@.svg').format(application_id), + install_dir: get_option('datadir') / 'icons' / scalable_dir +) + +symbolic_dir = 'hicolor' / 'symbolic' / 'apps' +install_data( + symbolic_dir / ('@0@-symbolic.svg').format(application_id), + install_dir: get_option('datadir') / 'icons' / symbolic_dir +) diff --git a/native/data/meson.build b/native/data/meson.build new file mode 100644 index 0000000..f525fa4 --- /dev/null +++ b/native/data/meson.build @@ -0,0 +1,27 @@ +desktop_utils = find_program('desktop-file-validate', required: false) +if desktop_utils.found() + test('Validate desktop file', desktop_utils, args: ['space.arimelody.LiveSchedule.desktop']) +endif + +appstreamcli = find_program('appstreamcli', required: false, disabler: true) +test('Validate appstream file', appstreamcli, + args: ['validate', '--no-net', '--explain', 'space.arimelody.LiveSchedule.metainfo.xml']) + +install_data('space.arimelody.LiveSchedule.gschema.xml', + install_dir: get_option('datadir') / 'glib-2.0' / 'schemas') + +compile_schemas = find_program('glib-compile-schemas', required: false, disabler: true) +test('Validate schema file', + compile_schemas, + args: ['--strict', '--dry-run', meson.current_source_dir()]) + +service_conf = configuration_data() +service_conf.set('bindir', get_option('prefix') / get_option('bindir')) +configure_file( + input: 'space.arimelody.LiveSchedule.service.in', + output: 'space.arimelody.LiveSchedule.service', + configuration: service_conf, + install_dir: get_option('datadir') / 'dbus-1' / 'services') + +subdir('resources') +subdir('icons') diff --git a/native/data/resources/application-window.ui b/native/data/resources/application-window.ui new file mode 100644 index 0000000..1aff620 --- /dev/null +++ b/native/data/resources/application-window.ui @@ -0,0 +1,12 @@ + + + + + diff --git a/native/data/resources/liveschedule.gresource.xml b/native/data/resources/liveschedule.gresource.xml new file mode 100644 index 0000000..1ce437d --- /dev/null +++ b/native/data/resources/liveschedule.gresource.xml @@ -0,0 +1,12 @@ + + + + application-window.ui + + + ../../../index.html + ../../../main.js + ../../../style.css + ../../../background.png + + diff --git a/native/data/resources/meson.build b/native/data/resources/meson.build new file mode 100644 index 0000000..9788e07 --- /dev/null +++ b/native/data/resources/meson.build @@ -0,0 +1,3 @@ +liveschedule_sources += gnome.compile_resources('ls-resources', + 'liveschedule.gresource.xml', + c_name: 'liveschedule') diff --git a/native/data/space.arimelody.LiveSchedule.desktop b/native/data/space.arimelody.LiveSchedule.desktop new file mode 100644 index 0000000..f313ae1 --- /dev/null +++ b/native/data/space.arimelody.LiveSchedule.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=LiveSchedule +Exec=LiveSchedule +Icon=space.arimelody.LiveSchedule +Terminal=false +Type=Application +Categories=Utility; +Keywords=GTK; +StartupNotify=true +DBusActivatable=true diff --git a/native/data/space.arimelody.LiveSchedule.gschema.xml b/native/data/space.arimelody.LiveSchedule.gschema.xml new file mode 100644 index 0000000..c18a8c3 --- /dev/null +++ b/native/data/space.arimelody.LiveSchedule.gschema.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/native/data/space.arimelody.LiveSchedule.metainfo.xml b/native/data/space.arimelody.LiveSchedule.metainfo.xml new file mode 100644 index 0000000..b2eaed0 --- /dev/null +++ b/native/data/space.arimelody.LiveSchedule.metainfo.xml @@ -0,0 +1,18 @@ + + + space.arimelody.LiveSchedule + CC0-1.0 + GPL-3.0-or-later + + Live Schedule + Render live schedules + +

Render live schedules

+
+ + + Ari Melody + + + +
diff --git a/native/data/space.arimelody.LiveSchedule.service.in b/native/data/space.arimelody.LiveSchedule.service.in new file mode 100644 index 0000000..e94464d --- /dev/null +++ b/native/data/space.arimelody.LiveSchedule.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=space.arimelody.LiveSchedule +Exec=@bindir@/schedule --gapplication-service diff --git a/native/meson.build b/native/meson.build new file mode 100644 index 0000000..962a61d --- /dev/null +++ b/native/meson.build @@ -0,0 +1,79 @@ +i18n = import('i18n') +gnome = import('gnome') +cc = meson.get_compiler('c') + +project_c_args = [] +test_c_args = [ + '-Wcast-align', + '-Wdeclaration-after-statement', + '-Werror=address', + '-Werror=array-bounds', + '-Werror=empty-body', + '-Werror=implicit', + '-Werror=implicit-function-declaration', + '-Werror=incompatible-pointer-types', + '-Werror=init-self', + '-Werror=int-conversion', + '-Werror=int-to-pointer-cast', + '-Werror=main', + '-Werror=misleading-indentation', + '-Werror=missing-braces', + '-Werror=missing-include-dirs', + '-Werror=nonnull', + '-Werror=overflow', + '-Werror=parenthesis', + '-Werror=pointer-arith', + '-Werror=pointer-to-int-cast', + '-Werror=redundant-decls', + '-Werror=return-type', + '-Werror=sequence-point', + '-Werror=shadow', + '-Werror=strict-prototypes', + '-Werror=trigraphs', + '-Werror=undef', + '-Werror=write-strings', + '-Wformat-nonliteral', + '-Wignored-qualifiers', + '-Wimplicit-function-declaration', + '-Wlogical-op', + '-Wmissing-declarations', + '-Wmissing-format-attribute', + '-Wmissing-include-dirs', + '-Wmissing-noreturn', + '-Wnested-externs', + '-Wno-cast-function-type', + '-Wno-dangling-pointer', + '-Wno-missing-field-initializers', + '-Wno-sign-compare', + '-Wno-unused-parameter', + '-Wold-style-definition', + '-Wpointer-arith', + '-Wredundant-decls', + '-Wstrict-prototypes', + '-Wswitch-default', + '-Wswitch-enum', + '-Wundef', + '-Wuninitialized', + '-Wunused', + '-fno-strict-aliasing', + ['-Werror=format-security', '-Werror=format=2'], +] +if get_option('buildtype') != 'plain' + test_c_args += '-fstack-protector-strong' +endif +foreach arg: test_c_args + if cc.has_multi_arguments(arg) + project_c_args += arg + endif +endforeach +add_project_arguments(project_c_args, language: 'c') + +liveschedule_sources = [] +subdir('data') +subdir('src') + +# TODO: UNCOMMENT WHEN YOU FIGURE OUT WHY THIS IS FAILING +#gnome.post_install( +# glib_compile_schemas: true, +# gtk_update_icon_cache: true, +# update_desktop_database: true) diff --git a/native/src/ls-application-window.c b/native/src/ls-application-window.c new file mode 100644 index 0000000..34c0fde --- /dev/null +++ b/native/src/ls-application-window.c @@ -0,0 +1,22 @@ +#include "config.h" + +#include "ls-application-window.h" +#include "ls-web-view.h" + +struct _LSApplicationWindow { + GtkApplicationWindow parent_instance; +}; + +G_DEFINE_FINAL_TYPE(LSApplicationWindow, ls_application_window, GTK_TYPE_APPLICATION_WINDOW) + +static void ls_application_window_class_init(LSApplicationWindowClass *klass) { + g_type_ensure(LS_TYPE_WEB_VIEW); + + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + + gtk_widget_class_set_template_from_resource(widget_class, "/space/arimelody/LiveSchedule/application-window.ui"); +} + +static void ls_application_window_init(LSApplicationWindow *self) { + gtk_widget_init_template(GTK_WIDGET(self)); +} diff --git a/native/src/ls-application-window.h b/native/src/ls-application-window.h new file mode 100644 index 0000000..0d69d31 --- /dev/null +++ b/native/src/ls-application-window.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define LS_TYPE_APPLICATION_WINDOW (ls_application_window_get_type()) + +G_DECLARE_FINAL_TYPE(LSApplicationWindow, + ls_application_window, + LS, APPLICATION_WINDOW, + GtkApplicationWindow) + +G_END_DECLS diff --git a/native/src/ls-application.c b/native/src/ls-application.c new file mode 100644 index 0000000..0d54b66 --- /dev/null +++ b/native/src/ls-application.c @@ -0,0 +1,46 @@ +#include "config.h" +#include + +#include "ls-application.h" +#include "ls-application-window.h" + +struct _LSApplication { + GtkApplication parent_instance; +}; + +G_DEFINE_FINAL_TYPE(LSApplication, ls_application, GTK_TYPE_APPLICATION) + +static void ls_application_activate(GApplication *app); + +static void ls_application_class_init (LSApplicationClass *klass) { + GApplicationClass *app_class = G_APPLICATION_CLASS(klass); + app_class->activate = ls_application_activate; +} + +LSApplication *ls_application_new(const char *application_id, + GApplicationFlags flags) +{ + g_return_val_if_fail(application_id != NULL, NULL); + + return g_object_new(LS_TYPE_APPLICATION, + "application-id", application_id, + "flags", flags, + "resource-base-path", "/space/arimelody/LiveSchedule", + NULL); +} + +static void ls_application_init(LSApplication *self) {} + +static void ls_application_activate(GApplication *app) { + GtkWindow *window; + + g_assert(LS_IS_APPLICATION(app)); + + window = gtk_application_get_active_window(GTK_APPLICATION(app)); + + if (window == NULL) + window = g_object_new(LS_TYPE_APPLICATION_WINDOW, "application", app, NULL); + + gtk_window_present(window); +} + diff --git a/native/src/ls-application.h b/native/src/ls-application.h new file mode 100644 index 0000000..761e558 --- /dev/null +++ b/native/src/ls-application.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define LS_TYPE_APPLICATION (ls_application_get_type()) + +G_DECLARE_FINAL_TYPE(LSApplication, ls_application, LS, APPLICATION, GtkApplication) + +LSApplication *ls_application_new(const char *application_id, + GApplicationFlags flags); + +G_END_DECLS diff --git a/native/src/ls-web-view.c b/native/src/ls-web-view.c new file mode 100644 index 0000000..1f18003 --- /dev/null +++ b/native/src/ls-web-view.c @@ -0,0 +1,284 @@ +#include "config.h" + +#include "ls-web-view.h" + +struct _LSWebView { + WebKitWebView parent_instance; + GCancellable *cancellable; +}; + +G_DEFINE_FINAL_TYPE(LSWebView, ls_web_view, WEBKIT_TYPE_WEB_VIEW) + +static void ls_web_view_constructed(GObject *object); + +static void ls_web_view_class_init(LSWebViewClass *klass) { + GObjectClass *object_class = G_OBJECT_CLASS(klass); + object_class->constructed = ls_web_view_constructed; +} + +static void ls_web_view_init(LSWebView *self) {} + +static void ls_web_view_on_resource_request(WebKitURISchemeRequest *request, + gpointer user_data); +static void ls_web_view_on_snapshot_message_received(WebKitUserContentManager *manager, + JSCValue *value, + gpointer user_data); + +static void ls_web_view_constructed(GObject *object) { + G_OBJECT_CLASS(ls_web_view_parent_class)->constructed(object); + + WebKitWebView *web_view = WEBKIT_WEB_VIEW(object); + + WebKitWebContext *web_context = webkit_web_view_get_context(web_view); + webkit_web_context_register_uri_scheme(web_context, "gresource", + ls_web_view_on_resource_request, + NULL, NULL); + + WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager(web_view); + g_signal_connect(manager, "script-message-received::copySnapshot", + G_CALLBACK(ls_web_view_on_snapshot_message_received), + g_object_ref(object)); + webkit_user_content_manager_register_script_message_handler(manager, "copySnapshot", NULL); + + webkit_web_view_load_uri(web_view, "gresource:///space/arimelody/LiveSchedule/webview/index.html"); +} + +static GdkTexture *ls_web_view_copy_snapshot_to_texture(GtkWidget *widget, + float xoffset, + float yoffset, + float width, + float height) +{ + GtkNative *native = gtk_widget_get_native(widget); + GskRenderer *renderer = gtk_native_get_renderer(native); + + GtkSnapshot *snapshot = gtk_snapshot_new(); + GTK_WIDGET_CLASS(G_OBJECT_GET_CLASS(widget))->snapshot(widget, snapshot); + GskRenderNode *node = gtk_snapshot_free_to_node(snapshot); + + graphene_rect_t bounds; + gsk_render_node_get_bounds(node, &bounds); + + bounds.origin.x += xoffset; + bounds.origin.y += yoffset; + bounds.size.width = width; + bounds.size.height = height; + + return gsk_renderer_render_texture(renderer, node, &bounds); +} + +struct _LSWebViewCopySnapshotUserData { + LSWebView *self; + GCancellable *cancellable; + guint callback_id; + float xoffset; + float yoffset; + float width; + float height; + GdkTexture *texture; +}; + +static void ls_web_view_copy_snapshot_to_clipboard(void *user_data) { + struct _LSWebViewCopySnapshotUserData *typed_user_data = (struct _LSWebViewCopySnapshotUserData *)user_data; + LSWebView *self = typed_user_data->self; + GCancellable *cancellable = typed_user_data->cancellable; + guint callback_id = typed_user_data->callback_id; + float xoffset = typed_user_data->xoffset; + float yoffset = typed_user_data->yoffset; + float width = typed_user_data->width; + float height = typed_user_data->height; + + if (g_cancellable_is_cancelled(cancellable)) { + WebKitUserMessage *message = + webkit_user_message_new("_nativePromiseReject", g_variant_new("(i)", callback_id)); + + webkit_web_view_send_message_to_page(WEBKIT_WEB_VIEW(self), message, cancellable, NULL, NULL); + + goto ls_webview_copy_snapshot_to_clipboard_exit; + } + + GdkTexture *texture = ls_web_view_copy_snapshot_to_texture(GTK_WIDGET(self), xoffset, yoffset, width, height); + + GdkClipboard *clipboard = gdk_display_get_clipboard(gdk_display_get_default()); + gdk_clipboard_set_texture(clipboard, texture); + + WebKitUserMessage *message = + webkit_user_message_new("_nativePromiseResolve", g_variant_new("(i)", callback_id)); + + webkit_web_view_send_message_to_page(WEBKIT_WEB_VIEW(self), message, NULL, NULL, NULL); + +ls_webview_copy_snapshot_to_clipboard_exit: + if (self->cancellable == cancellable) { + g_object_unref(self->cancellable); + self->cancellable = NULL; + } + + g_object_unref(self); + g_object_unref(cancellable); + g_free(user_data); +} + +static void ls_web_view_copy_snapshot_to_file_after_save_dialog(GObject *source, + GAsyncResult *result, + void *user_data) +{ + struct _LSWebViewCopySnapshotUserData *typed_user_data = (struct _LSWebViewCopySnapshotUserData *)user_data; + LSWebView *self = typed_user_data->self; + GCancellable *cancellable = typed_user_data->cancellable; + guint callback_id = typed_user_data->callback_id; + GdkTexture *texture = typed_user_data->texture; + + if (g_cancellable_is_cancelled(cancellable)) { + WebKitUserMessage *message = + webkit_user_message_new("_nativePromiseReject", g_variant_new("(i)", callback_id)); + + webkit_web_view_send_message_to_page(WEBKIT_WEB_VIEW(self), message, cancellable, NULL, NULL); + + goto ls_web_view_copy_snapshot_to_file_after_save_dialog_exit; + } + + GError *error = NULL; + GFile *file = gtk_file_dialog_save_finish(GTK_FILE_DIALOG(source), result, &error); + if (error) { + g_warning("File dialog error: %s", error->message); + g_error_free(error); + return; + } + + char *path = g_file_get_path(file); + gdk_texture_save_to_png(texture, path); + + WebKitUserMessage *message = + webkit_user_message_new("_nativePromiseResolve", g_variant_new("(i)", callback_id)); + + webkit_web_view_send_message_to_page(WEBKIT_WEB_VIEW(self), message, NULL, NULL, NULL); + +ls_web_view_copy_snapshot_to_file_after_save_dialog_exit: + if (self->cancellable == cancellable) { + g_object_unref(self->cancellable); + self->cancellable = NULL; + } + + g_object_unref(self); + g_object_unref(cancellable); + g_free(user_data); +} + +static void ls_web_view_copy_snapshot_to_file(void *user_data) { + struct _LSWebViewCopySnapshotUserData *typed_user_data = (struct _LSWebViewCopySnapshotUserData *)user_data; + LSWebView *self = typed_user_data->self; + GCancellable *cancellable = typed_user_data->cancellable; + guint callback_id = typed_user_data->callback_id; + float xoffset = typed_user_data->xoffset; + float yoffset = typed_user_data->yoffset; + float width = typed_user_data->width; + float height = typed_user_data->height; + + if (g_cancellable_is_cancelled(cancellable)) { + WebKitUserMessage *message = + webkit_user_message_new("_nativePromiseReject", g_variant_new("(i)", callback_id)); + + webkit_web_view_send_message_to_page(WEBKIT_WEB_VIEW(self), message, cancellable, NULL, NULL); + + if (self->cancellable == cancellable) { + g_object_unref(self->cancellable); + self->cancellable = NULL; + } + + g_object_unref(self); + g_object_unref(cancellable); + g_free(user_data); + return; + } + + typed_user_data->texture = ls_web_view_copy_snapshot_to_texture(GTK_WIDGET(self), xoffset, yoffset, width, height); + + GtkFileDialog *dialog = gtk_file_dialog_new(); + gtk_file_dialog_set_title(dialog, "Save Image"); + gtk_file_dialog_set_initial_name(dialog, "image.png"); + gtk_file_dialog_save(dialog, GTK_WINDOW(gtk_widget_get_root(GTK_WIDGET(self))), cancellable, + ls_web_view_copy_snapshot_to_file_after_save_dialog, user_data); +} + +static void ls_web_view_on_snapshot_message_received(WebKitUserContentManager *manager, + JSCValue *value, + gpointer user_data) +{ + JSCValue *id_val = jsc_value_object_get_property_at_index(value, 0); + JSCValue *kind_val = jsc_value_object_get_property_at_index(value, 1); + JSCValue *xoffset_val = jsc_value_object_get_property_at_index(value, 2); + JSCValue *yoffset_val = jsc_value_object_get_property_at_index(value, 3); + JSCValue *width_val = jsc_value_object_get_property_at_index(value, 4); + JSCValue *height_val = jsc_value_object_get_property_at_index(value, 5); + int callback_id = jsc_value_to_int32(id_val); + char *kind = jsc_value_to_string(kind_val); + float xoffset = (float)jsc_value_to_double(xoffset_val); + float yoffset = (float)jsc_value_to_double(yoffset_val); + float width = (float)jsc_value_to_double(width_val); + float height = (float)jsc_value_to_double(height_val); + + LSWebView *self = LS_WEB_VIEW(user_data); + + if (self->cancellable != NULL) { + g_cancellable_cancel(self->cancellable); + g_object_unref(self->cancellable); + } + self->cancellable = g_cancellable_new(); + + struct _LSWebViewCopySnapshotUserData *task_user_data = + (struct _LSWebViewCopySnapshotUserData *)g_malloc(sizeof(struct _LSWebViewCopySnapshotUserData)); + task_user_data->self = g_object_ref(self); + task_user_data->cancellable = g_object_ref(self->cancellable); + task_user_data->xoffset = xoffset; + task_user_data->yoffset = yoffset; + task_user_data->width = width; + task_user_data->height = height; + + if (strcmp(kind, "clipboard") == 0) { + g_idle_add_once(ls_web_view_copy_snapshot_to_clipboard, task_user_data); + } else { + g_idle_add_once(ls_web_view_copy_snapshot_to_file, task_user_data); + } + + g_object_unref(id_val); + g_object_unref(kind_val); + g_object_unref(xoffset_val); + g_object_unref(yoffset_val); + g_object_unref(width_val); + g_object_unref(height_val); + g_free(kind); +} + +static void ls_web_view_on_resource_request(WebKitURISchemeRequest *request, + gpointer user_data) +{ + const char *uri = webkit_uri_scheme_request_get_uri(request); + GUri *parsed = g_uri_parse(uri, G_URI_FLAGS_NONE, NULL); + const char *path = g_uri_get_path(parsed); + + GError *error = NULL; + GBytes *bytes = g_resources_lookup_data(path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error); + + if (error) { + g_print("Resource error: %s\n", error->message); + webkit_uri_scheme_request_finish_error(request, error); + g_error_free(error); + g_uri_unref(parsed); + return; + } + + gsize size; + const void *data = g_bytes_get_data(bytes, &size); + + GInputStream *stream = g_memory_input_stream_new_from_bytes(bytes); + + // guess the MIME type from the path + char *mime_type = g_content_type_guess(path, data, size, NULL); + + webkit_uri_scheme_request_finish(request, stream, size, mime_type); + + g_free(mime_type); + g_object_unref(stream); + g_bytes_unref(bytes); +} + diff --git a/native/src/ls-web-view.h b/native/src/ls-web-view.h new file mode 100644 index 0000000..49849c9 --- /dev/null +++ b/native/src/ls-web-view.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define LS_TYPE_WEB_VIEW (ls_web_view_get_type()) + +G_DECLARE_FINAL_TYPE(LSWebView, ls_web_view, LS, WEB_VIEW, WebKitWebView) + +G_END_DECLS diff --git a/native/src/main.c b/native/src/main.c new file mode 100644 index 0000000..1f0d7d1 --- /dev/null +++ b/native/src/main.c @@ -0,0 +1,8 @@ +#include "config.h" + +#include "ls-application.h" + +int main (int argc, char *argv[]) { + g_autoptr(LSApplication) app = ls_application_new("space.arimelody.LiveSchedule", G_APPLICATION_DEFAULT_FLAGS); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/native/src/meson.build b/native/src/meson.build new file mode 100644 index 0000000..d4d3c32 --- /dev/null +++ b/native/src/meson.build @@ -0,0 +1,21 @@ +config_h = configuration_data() +config_h.set_quoted('PACKAGE_VERSION', meson.project_version()) +configure_file(output: 'config.h', configuration: config_h) +add_project_arguments(['-I' + meson.project_build_root()], language: 'c') + +liveschedule_sources += [ + 'main.c', + 'ls-application.c', + 'ls-application-window.c', + 'ls-web-view.c', +] + +liveschedule_deps = [ + dependency('gtk4'), + dependency('javascriptcoregtk-6.0'), + dependency('webkitgtk-6.0'), +] + +executable('liveschedule', liveschedule_sources, + dependencies: liveschedule_deps, + install: true)