From bf75b8b652dfd1e23f928bfc3d95cfd697801f99 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Mon, 30 Mar 2026 19:39:30 -0400 Subject: [PATCH 1/7] Native UI --- aux/flatpak/space.arimelody.LiveSchedule.json | 38 +++ main.js | 66 +++- meson.build | 6 + .../apps/space.arimelody.Schedule.svg | 130 ++++++++ .../space.arimelody.Schedule-symbolic.svg | 1 + native/data/icons/meson.build | 13 + native/data/meson.build | 27 ++ native/data/resources/application-window.ui | 12 + .../data/resources/liveschedule.gresource.xml | 12 + native/data/resources/meson.build | 3 + .../data/space.arimelody.LiveSchedule.desktop | 10 + .../space.arimelody.LiveSchedule.gschema.xml | 5 + .../space.arimelody.LiveSchedule.metainfo.xml | 18 ++ .../space.arimelody.LiveSchedule.service.in | 3 + native/meson.build | 79 +++++ native/src/ls-application-window.c | 22 ++ native/src/ls-application-window.h | 14 + native/src/ls-application.c | 46 +++ native/src/ls-application.h | 14 + native/src/ls-web-view.c | 284 ++++++++++++++++++ native/src/ls-web-view.h | 14 + native/src/main.c | 8 + native/src/meson.build | 21 ++ 23 files changed, 841 insertions(+), 5 deletions(-) create mode 100644 aux/flatpak/space.arimelody.LiveSchedule.json create mode 100644 meson.build create mode 100644 native/data/icons/hicolor/scalable/apps/space.arimelody.Schedule.svg create mode 100644 native/data/icons/hicolor/symbolic/apps/space.arimelody.Schedule-symbolic.svg create mode 100644 native/data/icons/meson.build create mode 100644 native/data/meson.build create mode 100644 native/data/resources/application-window.ui create mode 100644 native/data/resources/liveschedule.gresource.xml create mode 100644 native/data/resources/meson.build create mode 100644 native/data/space.arimelody.LiveSchedule.desktop create mode 100644 native/data/space.arimelody.LiveSchedule.gschema.xml create mode 100644 native/data/space.arimelody.LiveSchedule.metainfo.xml create mode 100644 native/data/space.arimelody.LiveSchedule.service.in create mode 100644 native/meson.build create mode 100644 native/src/ls-application-window.c create mode 100644 native/src/ls-application-window.h create mode 100644 native/src/ls-application.c create mode 100644 native/src/ls-application.h create mode 100644 native/src/ls-web-view.c create mode 100644 native/src/ls-web-view.h create mode 100644 native/src/main.c create mode 100644 native/src/meson.build 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) -- 2.49.1 From ab7ad33086c2b3ebe77d346c09a545ec47cbdffc Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Tue, 31 Mar 2026 22:14:23 -0400 Subject: [PATCH 2/7] IT WORKS --- aux/flatpak/space.arimelody.LiveSchedule.json | 2 +- main.js | 66 +++++++++---------- native/src/ls-web-view.c | 5 +- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/aux/flatpak/space.arimelody.LiveSchedule.json b/aux/flatpak/space.arimelody.LiveSchedule.json index aad748e..9786ab7 100644 --- a/aux/flatpak/space.arimelody.LiveSchedule.json +++ b/aux/flatpak/space.arimelody.LiveSchedule.json @@ -1,7 +1,7 @@ { "id" : "space.arimelody.LiveSchedule", "runtime" : "org.gnome.Platform", - "runtime-version" : "48", + "runtime-version" : "master", "sdk" : "org.gnome.Sdk", "command" : "liveschedule", "finish-args" : [ diff --git a/main.js b/main.js index 2c034a2..60b92df 100644 --- a/main.js +++ b/main.js @@ -14,6 +14,8 @@ let schedule = { const offlineTitle = 'OFFLINE'; const offlineSubtitle = 'takin a nap'; +let copyClipboard; +let saveImage; document.addEventListener('readystatechange', () => { document.getElementById('version').innerText = VERSION; @@ -118,7 +120,7 @@ if ('webkit' in window && 'messageHandlers' in window.webkit) { let _promiseId = 0; let _promises = new Map(); - window.webkit.messageHandlers._nativePromiseResolved.addEventListener('message', (event) => { + window.addEventListener('message', (event) => { const [id] = event.detail; const [resolve,] = _promises.get(id) ?? []; if (!resolve) return; @@ -126,7 +128,7 @@ if ('webkit' in window && 'messageHandlers' in window.webkit) { resolve(); }); - window.webkit.messageHandlers._nativePromiseRejected.addEventListener('message', (event) => { + window.addEventListener('message', (event) => { const [id] = event.detail; const [,reject] = _promises.get(id) ?? []; if (!reject) return; @@ -134,45 +136,41 @@ if ('webkit' in window && 'messageHandlers' in window.webkit) { reject(); }); - function copyClipboard() { - return new Promise((resolve, reject) => { - const thisPromiseId = ++promiseId; + copyClipboard = () => new Promise((resolve, reject) => { + const thisPromiseId = ++_promiseId; - _promises.set(thisPromiseId, [resolve, reject]); + _promises.set(thisPromiseId, [resolve, reject]); - const schedule = document.getElementById('schedule'); - const boundingRect = getBoundingClientRect(); + const schedule = document.getElementById('schedule'); + const boundingRect = schedule.getBoundingClientRect(); - window.webkit.messageHandlers.copySnapshot.postMessage([ - thisPromiseId, - 'clipboard', - boundingRect.x, - boundingRect.y, - boundingRect.width, - boundingRect.height, - ]); - }); - } + window.webkit.messageHandlers.copySnapshot.postMessage([ + thisPromiseId, + 'clipboard', + boundingRect.x + 1, + boundingRect.y + 1, + boundingRect.width - 2, + boundingRect.height - 2, + ]); + }); - function saveImage() { - return new Promise((resolve, reject) => { - const thisPromiseId = ++promiseId; + saveImage = () => new Promise((resolve, reject) => { + const thisPromiseId = ++_promiseId; - _promises.set(thisPromiseId, [resolve, reject]); + _promises.set(thisPromiseId, [resolve, reject]); - const schedule = document.getElementById('schedule'); - const boundingRect = getBoundingClientRect(); + const schedule = document.getElementById('schedule'); + const boundingRect = schedule.getBoundingClientRect(); - window.webkit.messageHandlers.copySnapshot.postMessage([ - thisPromiseId, - 'file', - boundingRect.x, - boundingRect.y, - boundingRect.width, - boundingRect.height, - ]); - }); - } + window.webkit.messageHandlers.copySnapshot.postMessage([ + thisPromiseId, + 'file', + boundingRect.x + 1, + boundingRect.y + 1, + boundingRect.width - 2, + boundingRect.height - 2, + ]); + }); document.getElementById('clipboard').removeAttribute('disabled'); document.getElementById('save-image').removeAttribute('disabled'); diff --git a/native/src/ls-web-view.c b/native/src/ls-web-view.c index 1f18003..cbafdac 100644 --- a/native/src/ls-web-view.c +++ b/native/src/ls-web-view.c @@ -34,11 +34,14 @@ static void ls_web_view_constructed(GObject *object) { ls_web_view_on_resource_request, NULL, NULL); + WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(object)); + webkit_settings_set_enable_developer_extras(settings, TRUE); + WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager(web_view); + webkit_user_content_manager_register_script_message_handler(manager, "copySnapshot", NULL); 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"); } -- 2.49.1 From f37337f088b187d8ab805625291823c0de8e0576 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Tue, 31 Mar 2026 22:15:59 -0400 Subject: [PATCH 3/7] Fix indentation --- main.js | 92 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/main.js b/main.js index 60b92df..7cdcf6c 100644 --- a/main.js +++ b/main.js @@ -117,63 +117,63 @@ function altText() { } if ('webkit' in window && 'messageHandlers' in window.webkit) { - let _promiseId = 0; - let _promises = new Map(); + let _promiseId = 0; + let _promises = new Map(); - window.addEventListener('message', (event) => { - const [id] = event.detail; - const [resolve,] = _promises.get(id) ?? []; - if (!resolve) return; - _promises.delete(id); - resolve(); - }); + window.addEventListener('message', (event) => { + const [id] = event.detail; + const [resolve,] = _promises.get(id) ?? []; + if (!resolve) return; + _promises.delete(id); + resolve(); + }); - window.addEventListener('message', (event) => { - const [id] = event.detail; - const [,reject] = _promises.get(id) ?? []; - if (!reject) return; - _promises.delete(id); - reject(); - }); + window.addEventListener('message', (event) => { + const [id] = event.detail; + const [,reject] = _promises.get(id) ?? []; + if (!reject) return; + _promises.delete(id); + reject(); + }); - copyClipboard = () => new Promise((resolve, reject) => { - const thisPromiseId = ++_promiseId; + copyClipboard = () => new Promise((resolve, reject) => { + const thisPromiseId = ++_promiseId; - _promises.set(thisPromiseId, [resolve, reject]); + _promises.set(thisPromiseId, [resolve, reject]); - const schedule = document.getElementById('schedule'); - const boundingRect = schedule.getBoundingClientRect(); + const schedule = document.getElementById('schedule'); + const boundingRect = schedule.getBoundingClientRect(); - window.webkit.messageHandlers.copySnapshot.postMessage([ - thisPromiseId, - 'clipboard', - boundingRect.x + 1, - boundingRect.y + 1, - boundingRect.width - 2, - boundingRect.height - 2, - ]); - }); + window.webkit.messageHandlers.copySnapshot.postMessage([ + thisPromiseId, + 'clipboard', + boundingRect.x + 1, + boundingRect.y + 1, + boundingRect.width - 2, + boundingRect.height - 2, + ]); + }); - saveImage = () => new Promise((resolve, reject) => { - const thisPromiseId = ++_promiseId; + saveImage = () => new Promise((resolve, reject) => { + const thisPromiseId = ++_promiseId; - _promises.set(thisPromiseId, [resolve, reject]); + _promises.set(thisPromiseId, [resolve, reject]); - const schedule = document.getElementById('schedule'); - const boundingRect = schedule.getBoundingClientRect(); + const schedule = document.getElementById('schedule'); + const boundingRect = schedule.getBoundingClientRect(); - window.webkit.messageHandlers.copySnapshot.postMessage([ - thisPromiseId, - 'file', - boundingRect.x + 1, - boundingRect.y + 1, - boundingRect.width - 2, - boundingRect.height - 2, - ]); - }); + window.webkit.messageHandlers.copySnapshot.postMessage([ + thisPromiseId, + 'file', + boundingRect.x + 1, + boundingRect.y + 1, + boundingRect.width - 2, + boundingRect.height - 2, + ]); + }); - document.getElementById('clipboard').removeAttribute('disabled'); - document.getElementById('save-image').removeAttribute('disabled'); + document.getElementById('clipboard').removeAttribute('disabled'); + document.getElementById('save-image').removeAttribute('disabled'); } function exportJSON() { -- 2.49.1 From 708afaf4d374ef4f86bd9a267f06da7f90add3a7 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Tue, 31 Mar 2026 22:33:29 -0400 Subject: [PATCH 4/7] Copy alt to clipboard when running natively --- main.js | 19 +++++++++++++++-- native/src/ls-web-view.c | 46 ++++++++++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/main.js b/main.js index 7cdcf6c..4054288 100644 --- a/main.js +++ b/main.js @@ -14,6 +14,8 @@ let schedule = { const offlineTitle = 'OFFLINE'; const offlineSubtitle = 'takin a nap'; + +let altText; let copyClipboard; let saveImage; @@ -91,7 +93,7 @@ function loadSchedule() { schedule = JSON.parse(atob(saved)); } -function altText() { +function generateAltText() { let altText = "ari melody LIVE schedule: "; const activeDays = Object.keys(schedule.days) @@ -113,9 +115,11 @@ function altText() { altText += `, `; } - alert(altText); + return altText; } +altText = () => alert(generateAltText()); + if ('webkit' in window && 'messageHandlers' in window.webkit) { let _promiseId = 0; let _promises = new Map(); @@ -136,6 +140,17 @@ if ('webkit' in window && 'messageHandlers' in window.webkit) { reject(); }); + altText = () => new Promise((resolve, reject) => { + const thisPromiseId = ++_promiseId; + + _promises.set(thisPromiseId, [resolve, reject]); + + window.webkit.messageHandlers.altText.postMessage([ + thisPromiseId, + generateAltText(), + ]); + }); + copyClipboard = () => new Promise((resolve, reject) => { const thisPromiseId = ++_promiseId; diff --git a/native/src/ls-web-view.c b/native/src/ls-web-view.c index cbafdac..bed5332 100644 --- a/native/src/ls-web-view.c +++ b/native/src/ls-web-view.c @@ -20,9 +20,12 @@ 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, +static void ls_web_view_on_alt_text_message_received(WebKitUserContentManager *manager, JSCValue *value, gpointer user_data); +static void ls_web_view_on_copy_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); @@ -38,10 +41,14 @@ static void ls_web_view_constructed(GObject *object) { webkit_settings_set_enable_developer_extras(settings, TRUE); WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager(web_view); + + webkit_user_content_manager_register_script_message_handler(manager, "altText", NULL); + g_signal_connect(manager, "script-message-received::altText", + G_CALLBACK(ls_web_view_on_alt_text_message_received), object); + webkit_user_content_manager_register_script_message_handler(manager, "copySnapshot", NULL); g_signal_connect(manager, "script-message-received::copySnapshot", - G_CALLBACK(ls_web_view_on_snapshot_message_received), - g_object_ref(object)); + G_CALLBACK(ls_web_view_on_copy_snapshot_message_received), object); webkit_web_view_load_uri(web_view, "gresource:///space/arimelody/LiveSchedule/webview/index.html"); } @@ -203,17 +210,41 @@ static void ls_web_view_copy_snapshot_to_file(void *user_data) { ls_web_view_copy_snapshot_to_file_after_save_dialog, user_data); } -static void ls_web_view_on_snapshot_message_received(WebKitUserContentManager *manager, +static void ls_web_view_on_alt_text_message_received(WebKitUserContentManager *manager, JSCValue *value, gpointer user_data) { - JSCValue *id_val = jsc_value_object_get_property_at_index(value, 0); + JSCValue *callback_id_val = jsc_value_object_get_property_at_index(value, 0); + JSCValue *alt_text_val = jsc_value_object_get_property_at_index(value, 1); + int callback_id = jsc_value_to_int32(callback_id_val); + char *alt_text = jsc_value_to_string(alt_text_val); + + GdkClipboard *clipboard = gdk_display_get_clipboard(gdk_display_get_default()); + gdk_clipboard_set_text(clipboard, alt_text); + + LSWebView *self = LS_WEB_VIEW(user_data); + + 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); + + g_object_unref(callback_id_val); + g_object_unref(alt_text_val); + g_free(alt_text); +} + +static void ls_web_view_on_copy_snapshot_message_received(WebKitUserContentManager *manager, + JSCValue *value, + gpointer user_data) +{ + JSCValue *callback_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); + int callback_id = jsc_value_to_int32(callback_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); @@ -232,6 +263,7 @@ static void ls_web_view_on_snapshot_message_received(WebKitUserContentManager *m (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->callback_id = callback_id; task_user_data->xoffset = xoffset; task_user_data->yoffset = yoffset; task_user_data->width = width; @@ -243,7 +275,7 @@ static void ls_web_view_on_snapshot_message_received(WebKitUserContentManager *m g_idle_add_once(ls_web_view_copy_snapshot_to_file, task_user_data); } - g_object_unref(id_val); + g_object_unref(callback_id_val); g_object_unref(kind_val); g_object_unref(xoffset_val); g_object_unref(yoffset_val); -- 2.49.1 From 9983f9884da98fc6a7c1d6b5ce6f9b83be5554a6 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Tue, 31 Mar 2026 22:55:33 -0400 Subject: [PATCH 5/7] Remove unneeded promises --- main.js | 48 ++++-------------------------------- native/src/ls-web-view.c | 53 +++++----------------------------------- 2 files changed, 11 insertions(+), 90 deletions(-) diff --git a/main.js b/main.js index 4054288..2d1bbfa 100644 --- a/main.js +++ b/main.js @@ -121,71 +121,33 @@ function generateAltText() { altText = () => alert(generateAltText()); if ('webkit' in window && 'messageHandlers' in window.webkit) { - let _promiseId = 0; - let _promises = new Map(); - - window.addEventListener('message', (event) => { - const [id] = event.detail; - const [resolve,] = _promises.get(id) ?? []; - if (!resolve) return; - _promises.delete(id); - resolve(); - }); - - window.addEventListener('message', (event) => { - const [id] = event.detail; - const [,reject] = _promises.get(id) ?? []; - if (!reject) return; - _promises.delete(id); - reject(); - }); - - altText = () => new Promise((resolve, reject) => { - const thisPromiseId = ++_promiseId; - - _promises.set(thisPromiseId, [resolve, reject]); - - window.webkit.messageHandlers.altText.postMessage([ - thisPromiseId, - generateAltText(), - ]); - }); - - copyClipboard = () => new Promise((resolve, reject) => { - const thisPromiseId = ++_promiseId; - - _promises.set(thisPromiseId, [resolve, reject]); + altText = () => window.webkit.messageHandlers.altText.postMessage([generateAltText()]); + copyClipboard = () => { const schedule = document.getElementById('schedule'); const boundingRect = schedule.getBoundingClientRect(); window.webkit.messageHandlers.copySnapshot.postMessage([ - thisPromiseId, 'clipboard', boundingRect.x + 1, boundingRect.y + 1, boundingRect.width - 2, boundingRect.height - 2, ]); - }); - - saveImage = () => new Promise((resolve, reject) => { - const thisPromiseId = ++_promiseId; - - _promises.set(thisPromiseId, [resolve, reject]); + }; + saveImage = () => { const schedule = document.getElementById('schedule'); const boundingRect = schedule.getBoundingClientRect(); window.webkit.messageHandlers.copySnapshot.postMessage([ - thisPromiseId, 'file', boundingRect.x + 1, boundingRect.y + 1, boundingRect.width - 2, boundingRect.height - 2, ]); - }); + }; document.getElementById('clipboard').removeAttribute('disabled'); document.getElementById('save-image').removeAttribute('disabled'); diff --git a/native/src/ls-web-view.c b/native/src/ls-web-view.c index bed5332..23ad7ee 100644 --- a/native/src/ls-web-view.c +++ b/native/src/ls-web-view.c @@ -80,7 +80,6 @@ static GdkTexture *ls_web_view_copy_snapshot_to_texture(GtkWidget *widget, struct _LSWebViewCopySnapshotUserData { LSWebView *self; GCancellable *cancellable; - guint callback_id; float xoffset; float yoffset; float width; @@ -92,18 +91,12 @@ 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; } @@ -112,11 +105,6 @@ static void ls_web_view_copy_snapshot_to_clipboard(void *user_data) { 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); @@ -135,15 +123,9 @@ static void ls_web_view_copy_snapshot_to_file_after_save_dialog(GObject *so 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; } @@ -158,11 +140,6 @@ static void ls_web_view_copy_snapshot_to_file_after_save_dialog(GObject *so 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); @@ -178,18 +155,12 @@ 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; @@ -214,9 +185,7 @@ static void ls_web_view_on_alt_text_message_received(WebKitUserContentManager *m JSCValue *value, gpointer user_data) { - JSCValue *callback_id_val = jsc_value_object_get_property_at_index(value, 0); - JSCValue *alt_text_val = jsc_value_object_get_property_at_index(value, 1); - int callback_id = jsc_value_to_int32(callback_id_val); + JSCValue *alt_text_val = jsc_value_object_get_property_at_index(value, 0); char *alt_text = jsc_value_to_string(alt_text_val); GdkClipboard *clipboard = gdk_display_get_clipboard(gdk_display_get_default()); @@ -224,12 +193,6 @@ static void ls_web_view_on_alt_text_message_received(WebKitUserContentManager *m LSWebView *self = LS_WEB_VIEW(user_data); - 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); - - g_object_unref(callback_id_val); g_object_unref(alt_text_val); g_free(alt_text); } @@ -238,13 +201,11 @@ static void ls_web_view_on_copy_snapshot_message_received(WebKitUserContentManag JSCValue *value, gpointer user_data) { - JSCValue *callback_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(callback_id_val); + JSCValue *kind_val = jsc_value_object_get_property_at_index(value, 0); + JSCValue *xoffset_val = jsc_value_object_get_property_at_index(value, 1); + JSCValue *yoffset_val = jsc_value_object_get_property_at_index(value, 2); + JSCValue *width_val = jsc_value_object_get_property_at_index(value, 3); + JSCValue *height_val = jsc_value_object_get_property_at_index(value, 4); 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); @@ -263,7 +224,6 @@ static void ls_web_view_on_copy_snapshot_message_received(WebKitUserContentManag (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->callback_id = callback_id; task_user_data->xoffset = xoffset; task_user_data->yoffset = yoffset; task_user_data->width = width; @@ -275,7 +235,6 @@ static void ls_web_view_on_copy_snapshot_message_received(WebKitUserContentManag g_idle_add_once(ls_web_view_copy_snapshot_to_file, task_user_data); } - g_object_unref(callback_id_val); g_object_unref(kind_val); g_object_unref(xoffset_val); g_object_unref(yoffset_val); -- 2.49.1 From ac9393c2b3af4a7f873c2496223166a87e2e82ef Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Tue, 31 Mar 2026 23:23:15 -0400 Subject: [PATCH 6/7] =?UTF-8?q?The=20worlds=20most=20jank=20fix=20for=20hi?= =?UTF-8?q?dpi=20known=20to=20man=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/main.js b/main.js index 2d1bbfa..7fbd3e5 100644 --- a/main.js +++ b/main.js @@ -129,10 +129,10 @@ if ('webkit' in window && 'messageHandlers' in window.webkit) { window.webkit.messageHandlers.copySnapshot.postMessage([ 'clipboard', - boundingRect.x + 1, - boundingRect.y + 1, - boundingRect.width - 2, - boundingRect.height - 2, + boundingRect.x + 2, + boundingRect.y + 2, + boundingRect.width - 3, + boundingRect.height - 3, ]); }; @@ -142,10 +142,10 @@ if ('webkit' in window && 'messageHandlers' in window.webkit) { window.webkit.messageHandlers.copySnapshot.postMessage([ 'file', - boundingRect.x + 1, - boundingRect.y + 1, - boundingRect.width - 2, - boundingRect.height - 2, + boundingRect.x + 2, + boundingRect.y + 2, + boundingRect.width - 3, + boundingRect.height - 3, ]); }; -- 2.49.1 From 1897212f5bcc828c9e04081b8d1da85fd2224cdb Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 20 Jul 2026 22:24:00 +0100 Subject: [PATCH 7/7] add special event toggle (alt + click) --- main.js | 23 ++++++++++++++++++++++- style.css | 30 +++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/main.js b/main.js index 7fbd3e5..e42b01d 100644 --- a/main.js +++ b/main.js @@ -180,6 +180,8 @@ function createSchedule() { container.id = `slot-${day}`; if (!schedule.days[day].enabled) container.classList.add('disabled'); + if (schedule.days[day].special) + container.classList.add('special'); const timeslotDateContainer = document.createElement('div'); timeslotDateContainer.className = 'timeslot-date'; @@ -214,7 +216,10 @@ function createSchedule() { scheduleDOM.appendChild(container); - timeslotDay.addEventListener('click', () => { + timeslotDay.addEventListener('click', event => { + if (event.altKey) + return; + schedule.days[day].enabled = !schedule.days[day].enabled; timeslotTitle.contentEditable = schedule.days[day].enabled; @@ -241,5 +246,21 @@ function createSchedule() { timeslotTime.addEventListener('keyup', () => { schedule.days[day].time = timeslotTime.innerText; }); + + container.addEventListener('click', event => { + if (!event.altKey) + return; + + event.preventDefault(); + + const slot = schedule.days[day]; + slot.special = !slot.special; + + if (slot.special) { + container.classList.add("special"); + } else { + container.classList.remove("special"); + } + }); } } diff --git a/style.css b/style.css index 92c477c..0de1826 100644 --- a/style.css +++ b/style.css @@ -213,11 +213,35 @@ body { opacity: .75; } -.timeslot:not(.disabled) .timeslot-title, -.timeslot:not(.disabled) .timeslot-subtitle { - background: linear-gradient(to top, #81ac34 0%, #b7ff00 33%, #d7ff83 60%, #d9ff6d); +.timeslot:not(.disabled) { + .timeslot-title, .timeslot-subtitle { + background: linear-gradient(to top, #81ac34 0%, #b7ff00 33%, #d7ff83 60%, #d9ff6d); + /* --hue: 128.7; + background: linear-gradient(in oklch, to top, + oklch(0.58, 0.1464, var(--hue)) 0%, + oklch(0.70, 0.1764, var(--hue)) 33%, + oklch(0.95, 0.1164, var(--hue)) 60%, + oklch(0.90, 0.2264, var(--hue)) + ); */ -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: 0 0 8px color-mix(in srgb, var(--active), transparent 50%); } + +&.special { + .timeslot-title, .timeslot-subtitle { + --hue: 325.03; + + background: linear-gradient(to top, #bc11c7 0%, #d64bdf 33%, #f99cff 60%, #f788fe); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + + text-shadow: 0 0 8px color-mix(in srgb, #f788fe, transparent 50%); + } + + .timeslot-content::before { + background-color: #f788fe; + } +} +} -- 2.49.1