ui: overhaul sidebar, add content filters and state improvements

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.
This commit is contained in:
Jeena 2026-03-21 01:13:01 +00:00
parent 141f9ee32d
commit 8fd52dd8a0
16 changed files with 680 additions and 320 deletions

View file

@ -1,101 +1,131 @@
* {
box-sizing: border-box;
/* CSS custom properties are set from Rust via AdwStyleManager.
The :root defaults below act as a light-mode fallback only. */
:root {
--bg: #ffffff;
--fg: #1a1a1a;
--fg-dim: rgba(0,0,0,0.55);
--border: rgba(0,0,0,0.12);
--header-bg: #f6f5f4;
--link: #1c71d8;
--code-bg: rgba(0,0,0,0.06);
--blockquote-border: rgba(0,0,0,0.2);
--font: sans-serif;
--font-size: 15px;
}
body {
font-family: sans-serif;
:root[data-dark="1"] {
--bg: #1e1e1e;
--fg: rgba(255,255,255,0.87);
--fg-dim: rgba(255,255,255,0.5);
--border: rgba(255,255,255,0.12);
--header-bg: #242424;
--link: #78aeed;
--code-bg: rgba(255,255,255,0.06);
--blockquote-border: rgba(255,255,255,0.2);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
line-height: 1.6;
color: #222;
background: #fff;
background: var(--bg);
color: var(--fg);
font-family: var(--font);
font-size: var(--font-size);
word-wrap: break-word;
}
@media (prefers-color-scheme: dark) {
body {
color: #ddd;
background: #1e1e1e;
}
a {
color: #78aeed;
}
img {
opacity: 0.85;
}
a {
color: var(--link);
text-decoration: none;
}
#header {
article a {
text-decoration: underline;
}
header {
padding: 1.5em 2em 1em;
border-bottom: 1px solid rgba(0,0,0,0.1);
margin-bottom: 1em;
background: var(--header-bg);
border-bottom: 1px solid var(--border);
}
@media (prefers-color-scheme: dark) {
#header {
border-bottom-color: rgba(255,255,255,0.1);
}
header > .inner,
article {
max-width: 720px;
margin-left: auto;
margin-right: auto;
}
#feed-title {
font-size: 0.8em;
opacity: 0.6;
margin-bottom: 0.25em;
text-transform: uppercase;
letter-spacing: 0.05em;
header > .inner {
padding: 0 2em;
}
#title {
font-size: 1.5em;
margin: 0 0 0.5em;
header h1 {
font-size: 1.3em;
margin: 0.2em 0 0.4em;
padding: 0;
line-height: 1.3;
}
#meta {
font-size: 0.85em;
opacity: 0.6;
margin-bottom: 0.5em;
header h1 a {
color: var(--fg);
}
#meta span + span::before {
content: ' · ';
}
#link {
header p {
color: var(--fg-dim);
margin: 0;
padding: 0;
font-size: 0.85em;
}
#content {
padding: 0 2em 2em;
max-width: 800px;
article {
line-height: 1.6;
padding: 1.5em 2em 2em;
}
#content img {
img {
max-width: 100%;
height: auto;
}
#content pre {
overflow-x: auto;
background: rgba(0,0,0,0.05);
div > a:only-child img,
figure > a:only-child img,
p > a:only-child img,
figure > img:only-child,
div > img:only-child,
p > img:only-child {
display: block;
margin: 1em auto;
float: none !important;
}
pre {
overflow: auto;
background: var(--code-bg);
padding: 1em;
border-radius: 4px;
border-radius: 6px;
font-size: 0.9em;
}
@media (prefers-color-scheme: dark) {
#content pre {
background: rgba(255,255,255,0.05);
}
code {
background: var(--code-bg);
padding: 0.15em 0.35em;
border-radius: 3px;
font-size: 0.9em;
}
#content blockquote {
border-left: 3px solid rgba(0,0,0,0.2);
pre code {
background: none;
padding: 0;
}
blockquote {
border-left: 3px solid var(--blockquote-border);
margin-left: 0;
padding-left: 1em;
opacity: 0.8;
}
@media (prefers-color-scheme: dark) {
#content blockquote {
border-left-color: rgba(255,255,255,0.2);
}
color: var(--fg-dim);
font-style: italic;
}

View file

@ -2,35 +2,47 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="light dark">
<title>FeedTheMonkey</title>
<link rel="stylesheet" href="content.css">
<style>/*INJECT_CSS*/</style>
</head>
<body>
<div id="header">
<div id="feed-title"></div>
<h1 id="title"></h1>
<div id="meta">
<span id="author"></span>
<span id="date"></span>
<header>
<div class="inner">
<p><span id="feed_title"></span> <span id="author"></span></p>
<h1><a id="title" href=""></a></h1>
<p><time id="date"></time></p>
</div>
<a id="link" href="#" onclick="window.location='feedthemonkey:open'; return false;">Open in Browser</a>
</div>
<div id="content"></div>
</header>
<article id="article"></article>
<script>
function setArticle(article) {
document.getElementById('feed-title').textContent = article.feed_title || '';
document.getElementById('title').textContent = article.title || '';
document.getElementById('author').textContent = article.author || '';
document.getElementById('content').innerHTML = article.content || '';
var ts = article.updated;
if (ts) {
document.getElementById('date').textContent = new Date(ts * 1000).toLocaleDateString();
}
window.scrollTo(0, 0);
window._article = article;
document.getElementById('date').textContent = '';
document.getElementById('title').textContent = '';
document.getElementById('title').href = '';
document.getElementById('feed_title').textContent = '';
document.getElementById('author').textContent = '';
document.getElementById('article').innerHTML = '';
if (!article) return;
document.getElementById('date').textContent =
new Date(parseInt(article.updated, 10) * 1000).toLocaleDateString();
document.getElementById('title').textContent = article.title || '';
document.getElementById('title').href = article.link || '';
document.getElementById('feed_title').textContent = article.feed_title || '';
if (article.author && article.author.length > 0)
document.getElementById('author').textContent = '\u2013 ' + article.author;
document.getElementById('article').innerHTML = article.content || '';
}
function setDark(isDark) {
document.documentElement.setAttribute('data-dark', isDark ? '1' : '0');
}
function setFont(family, sizePx) {
document.documentElement.style.setProperty('--font', family);
document.documentElement.style.setProperty('--font-size', sizePx + 'px');
}
function checkKey(e) {