forked from jeena/FeedTheMonkey
Sidebar layout: - Replace AdwNavigationSplitView with GtkPaned for a resizable sidebar with a persistent width stored in GSettings. - Apply navigation-sidebar CSS class to the content Stack only (not the ToolbarView) so both header bars share the same colour and height. - Override Adwaita's automatic paned-first-child header tint and gap via application-level CSS. - Remove the gap between the sidebar header and the first list item. - Add toggle-sidebar button and F9 shortcut; sidebar visibility and width are persisted across restarts. Loading indicator: - Replace the large AdwSpinner status page + header Stack with a small Gtk.Spinner (16×16) in the header Stack so the header height never changes during loading. Article row: - Add hexpand to title and excerpt labels so text reflows when the sidebar is resized. Content: - Inline CSS into the HTML template at load time (/*INJECT_CSS*/ placeholder) so WebKit does not need a custom URI scheme handler. - Fix max-width centering and padding for article body and header. - Fix embedded video/iframe auto-opening in browser by checking NavigationType::LinkClicked instead of is_user_gesture(). Content filters: - Add Preferences dialog with a TextView for content-rewrite rules stored in GSettings (content-filters key). - Rule format: "domain find replace [find replace …]" one per line. - Rules are applied to article HTML before display and reloaded on every refresh. Shortcuts: - Add Ctrl+W to close, Ctrl+Q to quit, F1 for keyboard shortcuts overlay, j/k and arrow-key navigation via a capture-phase controller so keys work regardless of which widget has focus. Misc: - Set window title to "FeedTheMonkey" (fixes Hyprland title bar). - Update About dialog website URL.
163 lines
6.3 KiB
Rust
163 lines
6.3 KiB
Rust
use gtk4::prelude::*;
|
|
use libadwaita::prelude::*;
|
|
|
|
use crate::window::FeedTheMonkeyWindow;
|
|
|
|
const APP_ID: &str = "net.jeena.FeedTheMonkey";
|
|
|
|
glib::wrapper! {
|
|
pub struct FeedTheMonkeyApp(ObjectSubclass<imp::FeedTheMonkeyApp>)
|
|
@extends libadwaita::Application, gtk4::Application, gio::Application,
|
|
@implements gio::ActionGroup, gio::ActionMap;
|
|
}
|
|
|
|
impl FeedTheMonkeyApp {
|
|
pub fn new() -> Self {
|
|
glib::Object::builder()
|
|
.property("application-id", APP_ID)
|
|
.property("flags", gio::ApplicationFlags::empty())
|
|
.build()
|
|
}
|
|
|
|
pub fn run(&self) -> glib::ExitCode {
|
|
ApplicationExtManual::run(self)
|
|
}
|
|
}
|
|
|
|
mod imp {
|
|
use super::*;
|
|
use libadwaita::subclass::prelude::*;
|
|
|
|
#[derive(Default)]
|
|
pub struct FeedTheMonkeyApp;
|
|
|
|
#[glib::object_subclass]
|
|
impl ObjectSubclass for FeedTheMonkeyApp {
|
|
const NAME: &'static str = "FeedTheMonkeyApp";
|
|
type Type = super::FeedTheMonkeyApp;
|
|
type ParentType = libadwaita::Application;
|
|
}
|
|
|
|
impl ObjectImpl for FeedTheMonkeyApp {}
|
|
|
|
impl ApplicationImpl for FeedTheMonkeyApp {
|
|
fn activate(&self) {
|
|
self.parent_activate();
|
|
let app = self.obj();
|
|
|
|
// Register GResource
|
|
let resource_bytes = glib::Bytes::from_static(include_bytes!(env!("GRESOURCE_FILE")));
|
|
let resource = gio::Resource::from_data(&resource_bytes)
|
|
.expect("failed to load GResource");
|
|
gio::resources_register(&resource);
|
|
|
|
// Apply application-level CSS tweaks
|
|
let css = gtk4::CssProvider::new();
|
|
css.load_from_string(
|
|
"paned > :first-child .top-bar headerbar {
|
|
background-color: @headerbar_bg_color;
|
|
box-shadow: none;
|
|
}
|
|
paned > :first-child > toolbarview > .content {
|
|
padding-top: 0;
|
|
margin-top: 0;
|
|
}
|
|
.sidebar-content row:not(:selected) {
|
|
background-color: alpha(@window_fg_color, 0.07);
|
|
}
|
|
.sidebar-content row:not(:selected):hover {
|
|
background-color: alpha(@window_fg_color, 0.14);
|
|
}
|
|
.sidebar-content row:selected {
|
|
background-color: alpha(@window_fg_color, 0.22);
|
|
}"
|
|
);
|
|
gtk4::style_context_add_provider_for_display(
|
|
>k4::gdk::Display::default().unwrap(),
|
|
&css,
|
|
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
);
|
|
|
|
let window = FeedTheMonkeyWindow::new(app.upcast_ref());
|
|
|
|
// Shortcuts overlay
|
|
let builder = gtk4::Builder::from_resource(
|
|
"/net/jeena/FeedTheMonkey/ui/shortcuts.ui",
|
|
);
|
|
let overlay: gtk4::ShortcutsWindow = builder.object("help_overlay").unwrap();
|
|
window.set_help_overlay(Some(&overlay));
|
|
|
|
setup_shortcuts(&window);
|
|
|
|
// About action on app
|
|
let app_weak = app.downgrade();
|
|
let about_action = gio::SimpleAction::new("about", None);
|
|
about_action.connect_activate(move |_, _| {
|
|
if let Some(app) = app_weak.upgrade() {
|
|
let win = app.active_window();
|
|
let dialog = libadwaita::AboutDialog::builder()
|
|
.application_name("FeedTheMonkey")
|
|
.application_icon("feedthemonkey")
|
|
.version("3.0.0")
|
|
.copyright("© Jeena Paradies")
|
|
.license_type(gtk4::License::Gpl30)
|
|
.website("https://git.jeena.net/jeena/FeedTheMonkey")
|
|
.developer_name("Jeena Paradies")
|
|
.build();
|
|
dialog.present(win.as_ref().map(|w| w.upcast_ref::<gtk4::Widget>()));
|
|
}
|
|
});
|
|
app.add_action(&about_action);
|
|
|
|
// Quit action
|
|
let app_weak = app.downgrade();
|
|
let quit_action = gio::SimpleAction::new("quit", None);
|
|
quit_action.connect_activate(move |_, _| {
|
|
if let Some(app) = app_weak.upgrade() {
|
|
app.quit();
|
|
}
|
|
});
|
|
app.add_action(&quit_action);
|
|
|
|
window.present();
|
|
}
|
|
}
|
|
|
|
impl GtkApplicationImpl for FeedTheMonkeyApp {}
|
|
impl AdwApplicationImpl for FeedTheMonkeyApp {}
|
|
}
|
|
|
|
fn setup_shortcuts(window: &FeedTheMonkeyWindow) {
|
|
use gtk4::gdk::{Key, ModifierType};
|
|
|
|
let controller = gtk4::ShortcutController::new();
|
|
controller.set_scope(gtk4::ShortcutScope::Global);
|
|
|
|
let add = |controller: >k4::ShortcutController,
|
|
key: Key,
|
|
mods: ModifierType,
|
|
action_name: &str| {
|
|
let trigger = gtk4::KeyvalTrigger::new(key, mods);
|
|
let action = gtk4::NamedAction::new(action_name);
|
|
let shortcut = gtk4::Shortcut::new(Some(trigger), Some(action));
|
|
controller.add_shortcut(shortcut);
|
|
};
|
|
|
|
// j/k/Left/Right are handled by a capture-phase key controller in window.rs
|
|
// so they work regardless of which widget has focus.
|
|
add(&controller, Key::r, ModifierType::empty(), "win.reload");
|
|
add(&controller, Key::u, ModifierType::empty(), "win.mark-unread");
|
|
add(&controller, Key::Return, ModifierType::empty(), "win.open-in-browser");
|
|
add(&controller, Key::n, ModifierType::empty(), "win.open-in-browser");
|
|
add(&controller, Key::plus, ModifierType::CONTROL_MASK, "win.zoom-in");
|
|
add(&controller, Key::equal, ModifierType::CONTROL_MASK, "win.zoom-in");
|
|
add(&controller, Key::minus, ModifierType::CONTROL_MASK, "win.zoom-out");
|
|
add(&controller, Key::_0, ModifierType::CONTROL_MASK, "win.zoom-reset");
|
|
add(&controller, Key::F9, ModifierType::empty(), "win.toggle-sidebar");
|
|
add(&controller, Key::F11, ModifierType::empty(), "win.toggle-fullscreen");
|
|
add(&controller, Key::w, ModifierType::CONTROL_MASK, "window.close");
|
|
add(&controller, Key::q, ModifierType::CONTROL_MASK, "app.quit");
|
|
add(&controller, Key::F1, ModifierType::empty(), "win.show-help-overlay");
|
|
|
|
window.add_controller(controller);
|
|
}
|