diff --git a/Cargo.lock b/Cargo.lock index 99877fb..e006a81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1658,6 +1658,7 @@ dependencies = [ "relm4-macros", "serde", "serde_json", + "sourceview5", "tracing", "tracing-subscriber", "ureq", @@ -2397,6 +2398,41 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "sourceview5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c5f976a113e947bc5ec67758b2960c0db4ca76f80fb410d7cd86cd456d9ee5" +dependencies = [ + "futures-channel", + "futures-core", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "gtk4", + "libc", + "pango", + "sourceview5-sys", +] + +[[package]] +name = "sourceview5-sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29637cccd56075a37ba72c0cc8b8d599dbc1d857e30dadea97eaacbc29b7fd46" +dependencies = [ + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk4-sys", + "libc", + "pango-sys", + "system-deps", +] + [[package]] name = "spin" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 20ad0ec..ce9ebb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,8 @@ members = [ [dependencies] ntfy-daemon = { path = "./ntfy-daemon" } gettext-rs = { version = "0.7", features = ["gettext-system"] } -gtk = { version = "0.7", package = "gtk4", features = ["v4_12", "blueprint"] } +gtk = { version = "0.7", package = "gtk4", features = ["gnome_45"] } +gsv = { package = "sourceview5", version = "0.7" } once_cell = "1.14" tracing = "0.1.37" tracing-subscriber = "0.3" diff --git a/data/resources/style.css b/data/resources/style.css index 0f82a8c..171e09f 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -3,10 +3,17 @@ font-weight: bold; } +button.small { + padding: 6px 12px; + min-height: 0px; + min-width: 0px; + font-size: 14px; +} + .chip { min-height: 16px; min-width: 16px; - padding: 3px 5px; + padding: 2px 5px; color: @theme_fg_color; border-radius: 8px; } @@ -29,3 +36,23 @@ .chip--small { font-size: 0.8rem; } + + +.sourceview { + padding: 4px 8px; +} + +.code { + border-radius: 12px; + border: 1px solid @borders; +} + +.message_bar { + padding: 2px 2px; + background-color: @sidebar_bg_color; + border-radius: 24px; +} +.message_bar entry { + background-color: @sidebar_bg_color; + border-radius: 12px; +} diff --git a/data/resources/ui/window.blp b/data/resources/ui/window.blp index 5cce952..19ab375 100644 --- a/data/resources/ui/window.blp +++ b/data/resources/ui/window.blp @@ -127,16 +127,19 @@ template $NotifyWindow : Adw.ApplicationWindow { }; [bottom] Adw.Bin { - margin-top: 8; - margin-bottom: 8; - margin-start: 8; - margin-end: 8; + margin-top: 4; + margin-bottom: 4; + margin-start: 4; + margin-end: 4; Adw.Clamp { Gtk.Box { - spacing: 4; + styles [ + "message_bar" + ] Gtk.Button code_btn { styles [ - "circular" + "circular", + "flat" ] icon-name: "code-symbolic"; } diff --git a/meson.build b/meson.build index d0ee243..92c38a2 100644 --- a/meson.build +++ b/meson.build @@ -14,6 +14,7 @@ base_id = 'com.ranfdev.Notify' dependency('glib-2.0', version: '>= 2.66') dependency('gio-2.0', version: '>= 2.66') dependency('gtk4', version: '>= 4.0.0') +dependency('gtksourceview-5', version: '>= 5.0.0') glib_compile_resources = find_program('glib-compile-resources', required: true) glib_compile_schemas = find_program('glib-compile-schemas', required: true) diff --git a/src/widgets/window.rs b/src/widgets/window.rs index e729f67..290dc6e 100644 --- a/src/widgets/window.rs +++ b/src/widgets/window.rs @@ -4,6 +4,7 @@ use std::cell::OnceCell; use adw::prelude::*; use adw::subclass::prelude::*; use futures::prelude::*; +use gsv::prelude::*; use gtk::{gio, glib}; use ntfy_daemon::models; use ntfy_daemon::ntfy_capnp::{system_notifier, Status}; @@ -26,13 +27,17 @@ impl> SpawnWithToast for W { &self, f: impl Future> + 'static, ) { - let p: Option = self.ancestor(NotifyWindow::static_type()).and_downcast(); + let toast_overlay: Option = self + .ancestor(adw::ToastOverlay::static_type()) + .and_downcast(); + let win: Option = self.ancestor(NotifyWindow::static_type()).and_downcast(); glib::MainContext::default().spawn_local(async move { if let Err(e) = f.await { - if let Some(p) = p { - p.imp() - .toast_overlay - .add_toast(adw::Toast::builder().title(&e.to_string()).build()) + if let Some(o) = toast_overlay + .as_ref() + .or_else(|| win.as_ref().map(|win| win.imp().toast_overlay.as_ref())) + { + o.add_toast(adw::Toast::builder().title(&e.to_string()).build()) } } }); @@ -255,13 +260,6 @@ impl NotifyWindow { let this = self.clone(); let topic = self.selected_subscription().unwrap().topic(); let message = imp.entry.text(); - let buffer = gtk::TextBuffer::new(None); - buffer.set_text(&format!( - r#"{{ -"topic": "{topic}", -"message": "{message}" -}}"# - )); relm4_macros::view! { window = adw::Window { set_modal: true, @@ -270,50 +268,127 @@ impl NotifyWindow { set_content = &adw::ToolbarView { add_top_bar = &adw::HeaderBar {}, #[wrap(Some)] - set_content = &adw::Clamp { + set_content: toast_overlay = &adw::ToastOverlay { #[wrap(Some)] - set_child = >k::Box { - set_margin_top: 8, - set_margin_bottom: 8, - set_margin_start: 8, - set_margin_end: 8, - set_spacing: 8, - set_orientation: gtk::Orientation::Vertical, - append = >k::Label { - set_label: "Here you can manually build the JSON message you want to POST to this topic", - set_xalign: 0.0, - set_halign: gtk::Align::Start, - set_wrap_mode: gtk::pango::WrapMode::WordChar, - set_wrap: true, - }, - append: text_view = >k::TextView { - set_top_margin: 8, - set_bottom_margin: 8, - set_left_margin: 8, - set_right_margin: 8, - set_hexpand: true, - set_vexpand: true, - set_buffer: Some(&buffer), - }, - append = >k::Box { - set_halign: gtk::Align::Center, + set_child = &adw::Clamp { + #[wrap(Some)] + set_child = >k::Box { + set_margin_top: 8, + set_margin_bottom: 8, + set_margin_start: 8, + set_margin_end: 8, set_spacing: 8, - append = >k::Button { - add_css_class: "pill", - set_label: "Documentation", - connect_clicked[this] => move |_| { - gtk::UriLauncher::new("https://docs.ntfy.sh/publish/#publish-as-json").launch( - Some(&this), - gio::Cancellable::NONE, - |_| {} - ); - } + set_orientation: gtk::Orientation::Vertical, + append = >k::Label { + set_label: "Here you can manually build the JSON message you want to POST to this topic", + set_natural_wrap_mode: gtk::NaturalWrapMode::None, + set_xalign: 0.0, + set_halign: gtk::Align::Start, + set_wrap_mode: gtk::pango::WrapMode::WordChar, + set_wrap: true, + }, + append = >k::Label { + add_css_class: "heading", + set_label: "JSON", + set_xalign: 0.0, + set_halign: gtk::Align::Start, + }, + append: text_view = &gsv::View { + add_css_class: "code", + set_tab_width: 4, + set_indent_width: 2, + set_auto_indent: true, + set_top_margin: 4, + set_bottom_margin: 4, + set_left_margin: 4, + set_right_margin: 4, + set_hexpand: true, + set_vexpand: true, + set_monospace: true, + set_background_pattern: gsv::BackgroundPatternType::Grid + }, + append = >k::Label { + add_css_class: "heading", + set_label: "Snippets", + set_xalign: 0.0, + set_halign: gtk::Align::Start, + }, + append = >k::Box { + set_spacing: 4, + append = >k::Button { + add_css_class: "pill", + add_css_class: "small", + set_label: "Title", + connect_clicked[text_view] => move |_| { + text_view.buffer().insert_at_cursor(r#""title": "Title of your message""#) + } + }, + append = >k::Button { + add_css_class: "pill", + add_css_class: "small", + set_label: "Tags", + connect_clicked[text_view] => move |_| { + text_view.buffer().insert_at_cursor(r#""tags": ["warning","cd"]"#) + } + }, + append = >k::Button { + add_css_class: "pill", + add_css_class: "small", + set_label: "Priority", + connect_clicked[text_view] => move |_| { + text_view.buffer().insert_at_cursor(r#""priority": 5"#) + } + }, + append = >k::Button { + add_css_class: "pill", + add_css_class: "small", + set_label: "View Action", + connect_clicked[text_view] => move |_| { + text_view.buffer().insert_at_cursor(r#""action": [ + { + "type": "view", + "label": "torvalds boosted your toot", + "url": "https://joinmastodon.org" + } + ]"#) + } + }, + append = >k::Button { + add_css_class: "pill", + add_css_class: "small", + set_label: "HTTP Action", + connect_clicked[text_view] => move |_| { + text_view.buffer().insert_at_cursor(r#""action": [ + { + "type": "http", + "label": "Turn off lights", + "method": "post", + "url": "https://api.example.com/lights", + "body": "OFF" + } + ]"#) + } + }, + append = >k::Button { + add_css_class: "circular", + add_css_class: "small", + set_label: "?", + connect_clicked[this] => move |_| { + gtk::UriLauncher::new("https://docs.ntfy.sh/publish/#publish-as-json").launch( + Some(&this), + gio::Cancellable::NONE, + |_| {} + ); + } + }, }, append = >k::Button { + set_margin_top: 8, + set_margin_bottom: 8, add_css_class: "suggested-action", add_css_class: "pill", set_label: "Send", - connect_clicked[this, text_view] => move |_| { + connect_clicked[this, toast_overlay, text_view] => move |_| { let thisc = this.clone(); let text_view = text_view.clone(); let f = async move { @@ -327,7 +402,7 @@ impl NotifyWindow { .unwrap() .publish_msg(msg).await }; - this.spawn_with_near_toast(f); + toast_overlay.spawn_with_near_toast(f); } } } @@ -336,6 +411,26 @@ impl NotifyWindow { } } } + + let lang = gsv::LanguageManager::default().language("json").unwrap(); + let buffer = gsv::Buffer::with_language(&lang); + buffer.set_text(&format!( + r#"{{ + "topic": "{topic}", + "message": "{message}" +}}"# + )); + text_view.set_buffer(Some(&buffer)); + + let manager = adw::StyleManager::default(); + let scheme_name = if manager.is_dark() { + "solarized-dark" + } else { + "solarized-light" + }; + let scheme = gsv::StyleSchemeManager::default().scheme(scheme_name); + buffer.set_style_scheme(scheme.as_ref()); + window.present(); } fn show_subscription_info(&self) {