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 @@
+
+
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 @@
+
+
+
+
+ Live Schedule
+ 1400
+ 900
+
+
+
+
+
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)