From 284ba04f7968e6afb4ef5c92075305e6b9e764ff Mon Sep 17 00:00:00 2001 From: Marius Date: Thu, 11 Jun 2026 20:28:51 -0400 Subject: [PATCH] A grid/card view for displaying feeds with images, configurable 2-4 columns --- xExtension-GridView/LICENSE | 17 + xExtension-GridView/README.md | 115 ++ xExtension-GridView/configure.phtml | 98 + xExtension-GridView/extension.php | 516 +++++ xExtension-GridView/i18n/de/ext.php | 26 + xExtension-GridView/i18n/en/ext.php | 26 + xExtension-GridView/i18n/fr/ext.php | 26 + xExtension-GridView/metadata.json | 8 + xExtension-GridView/static/grid.css | 781 ++++++++ xExtension-GridView/static/grid.js | 1974 ++++++++++++++++++++ xExtension-GridView/static/placeholder.jpg | Bin 0 -> 116480 bytes 11 files changed, 3587 insertions(+) create mode 100644 xExtension-GridView/LICENSE create mode 100644 xExtension-GridView/README.md create mode 100644 xExtension-GridView/configure.phtml create mode 100644 xExtension-GridView/extension.php create mode 100644 xExtension-GridView/i18n/de/ext.php create mode 100644 xExtension-GridView/i18n/en/ext.php create mode 100644 xExtension-GridView/i18n/fr/ext.php create mode 100644 xExtension-GridView/metadata.json create mode 100644 xExtension-GridView/static/grid.css create mode 100644 xExtension-GridView/static/grid.js create mode 100644 xExtension-GridView/static/placeholder.jpg diff --git a/xExtension-GridView/LICENSE b/xExtension-GridView/LICENSE new file mode 100644 index 0000000..008e987 --- /dev/null +++ b/xExtension-GridView/LICENSE @@ -0,0 +1,17 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2024 FreshRSS User + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/xExtension-GridView/README.md b/xExtension-GridView/README.md new file mode 100644 index 0000000..d919576 --- /dev/null +++ b/xExtension-GridView/README.md @@ -0,0 +1,115 @@ +# Grid View Extension for FreshRSS + +A card/grid view extension for [FreshRSS](https://github.com/FreshRSS/FreshRSS) that displays feed entries in a responsive multi-column layout with prominent images. + +## Features + +- **Card-based layout**: Displays entries as cards with thumbnails, titles, source info, and descriptions +- **Configurable columns**: Choose between 2, 3, or 4 columns via the extension settings +- **Responsive design**: Automatically adjusts to fewer columns on tablets and mobile +- **Smart thumbnail extraction**: Uses feed thumbnails when available, falls back to extracting images from content (filters out small images <400x400, logos, icons, and theme assets) +- **Open Graph image fetching**: Optionally fetches OG images from article pages that have no thumbnail in the RSS feed (async with max 3 concurrent fetches) +- **Category/feed header**: Shows the current category or feed name at the top of the grid ("Main Stream" when viewing all feeds) +- **Action bar overlay**: Action buttons (mark read, star, share, open) appear as a transparent overlay on the card thumbnail on hover (desktop) or tap (mobile) +- **Mobile-friendly**: Tapping the article title opens the link directly without requiring card selection first; tapping elsewhere on the card toggles the action bar +- **Mobile sidebar toggle**: Optional floating hamburger button on mobile screens to open the FreshRSS sidebar without scrolling to the top +- **Browser Reader Mode**: Opens articles in Firefox Reader Mode (or equivalent) for distraction-free reading +- **FreshRSS Native Share**: Integrated share dropdown using your configured sharing services +- **Star/Favorite support**: Mark articles as favorites directly from the card with AJAX sync +- **Sort by publication date**: Optional setting to default sorting to publication date (newest first) +- **Keyboard shortcut**: Press "G" to toggle grid view on/off +- **Dark theme support**: Works seamlessly with FreshRSS dark themes +- **Persistent preference**: Your grid view state is saved locally across sessions +- **No flash of list view**: Seamless transitions during AJAX feed navigation with FOLV prevention + +## Installation + +1. Download or clone this repository +2. Copy the `xExtension-GridView` folder to your FreshRSS `extensions/` directory +3. In FreshRSS, go to **Settings → Extensions** +4. Enable the "Grid View" extension + +## Usage + +1. After enabling the extension, go to its configuration page to set your preferred options +2. Click the **grid icon (▦)** in the header area or press **"G"** on your keyboard to toggle grid view +3. Click on any card to open the article in browser Reader Mode (Firefox) or a new tab +4. Hover over a card (or tap on mobile) to reveal the action buttons: + - **✓ Mark as Read**: Toggle read/unread state + - **★ Star**: Mark the article as a favorite (syncs with FreshRSS) + - **🔗 Share**: Opens FreshRSS native share dropdown with your configured sharing services + - **↗ Open**: Open the original article in a new tab +5. Articles are automatically marked as read when you click on them + +Your grid view preference is saved locally and persists across sessions. + +## Configuration + +| Option | Description | Default | +| ------ | ----------- | ------- | +| Number of columns | How many columns to display (2-4) | 3 | +| Thumbnail fetching | Fetch Open Graph images from article pages that lack thumbnails | Off | +| Default sorting | Sort by publication date, newest first | Off | +| Mobile menu button | Show a floating sidebar toggle button on mobile screens | Off | +| Sticky navigation bar | Keep the top navigation bar visible while scrolling | On | + +## Requirements + +- FreshRSS 1.20.0 or later +- PHP 8.1 or later + +## Responsive Breakpoints + +| Screen Width | Columns | +| ------------ | ------- | +| > 1200px | Configured value (2-4) | +| 900px - 1200px | Up to 3 | +| 600px - 900px | 2 | +| < 600px | 1 | + +## Development + +### File Structure + +```text +xExtension-GridView/ +├── metadata.json # Extension metadata +├── extension.php # Main PHP class (hooks, OG image fetching, config) +├── configure.phtml # Configuration form +├── static/ +│ ├── grid.css # Grid layout styles +│ ├── grid.js # Card transformation + state sync logic +│ └── placeholder.jpg # Fallback thumbnail image +├── i18n/ +│ ├── en/ext.php # English translations +│ ├── fr/ext.php # French translations +│ └── de/ext.php # German translations +├── LICENSE +└── README.md +``` + +### How It Works + +1. The extension registers hooks during `init()`: `js_vars` injects configuration, `entry_before_display` injects thumbnail markers, and optionally `entry_before_insert` fetches OG images +2. JavaScript adds a toggle button and listens for the "G" keyboard shortcut +3. When grid view is enabled, the stream container gets a `.grid` class and a `gridview-active` class is added to `body` (for FOLV prevention) +4. A context header is inserted showing the current category, feed name, or "Main Stream" +5. FreshRSS date separator `.transition` elements are hidden in grid mode +6. JavaScript transforms existing `.flux` elements into card format with: + - Thumbnail extraction (with smart filtering for size and type) + - Action bar overlay on the thumbnail (visible on hover/tap) + - Star button with AJAX sync via FreshRSS bookmark links + - Share button using FreshRSS native share dropdown +7. CSS applies the grid layout using CSS Grid with `--gridview-columns` custom property +8. MutationObservers watch for dynamically loaded entries, stream replacements, and state changes (read/unread/favorite) +9. Click handlers open articles in browser Reader Mode and mark as read +10. On mobile, tapping the article title opens the link directly; tapping elsewhere toggles the action bar +11. When the mobile menu button setting is enabled, a floating hamburger button appears at the bottom-left on screens under 841px, toggling the FreshRSS sidebar via `toggle_aside_click()` + +## License + +AGPL-3.0 - See [LICENSE](LICENSE) for details. + +## Credits + +- Built for [FreshRSS](https://github.com/FreshRSS/FreshRSS) diff --git a/xExtension-GridView/configure.phtml b/xExtension-GridView/configure.phtml new file mode 100644 index 0000000..de58776 --- /dev/null +++ b/xExtension-GridView/configure.phtml @@ -0,0 +1,98 @@ +getColumns(); +$fetchOgImage = $this->isOgImageFetchEnabled(); +$sortByDate = $this->isSortByPublicationDateEnabled(); +$mobileMenuButton = $this->isMobileMenuButtonEnabled(); +$stickyNav = $this->isStickyNavEnabled(); +?> + +
+ + +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+
+ + +
+
+
+ +
+ +
+

+
+
diff --git a/xExtension-GridView/extension.php b/xExtension-GridView/extension.php new file mode 100644 index 0000000..f152ec7 --- /dev/null +++ b/xExtension-GridView/extension.php @@ -0,0 +1,516 @@ +getFileUrl('grid.css')); + Minz_View::appendScript($this->getFileUrl('grid.js')); + + // Register translations + $this->registerTranslates(); + + // Set default sorting to Publication Date, newest first (if enabled) + if ($this->isSortByPublicationDateEnabled()) { + $this->applyDefaultSort(); + } + + // Inject column count and settings as JS variable + $this->registerHook('js_vars', [$this, 'injectJsVars']); + + // Inject thumbnail URL into content so it's available in all view modes + // (Reading View doesn't render the .item.thumbnail element) + $this->registerHook('entry_before_display', [$this, 'injectThumbnailMarker']); + + // Fetch OG image for entries that have no thumbnail (user-configurable) + if ($this->isOgImageFetchEnabled()) { + $this->registerHook('entry_before_insert', [$this, 'fetchOgImage']); + // Instead of blocking page render to fetch OG images, inject a + // marker that the JS picks up and fetches asynchronously. + $this->registerHook('entry_before_display', [$this, 'markEntryForOgFetch']); + } + } + + /** + * Read a single user configuration value. + * + * Uses the array-based configuration API for compatibility with + * FreshRSS 1.28.x and earlier, which lack the typed getters. + * + * @return mixed + */ + private function cfgValue(string $key): mixed { + /** @phpstan-ignore method.deprecated */ + return $this->getUserConfigurationValue($key); + } + + /** + * Read the full user configuration array. + * + * @return array + */ + private function cfgAll(): array { + /** @phpstan-ignore method.deprecated */ + return $this->getUserConfiguration(); + } + + /** + * Persist the full user configuration array. + * + * @param array $config + */ + private function cfgSave(array $config): void { + /** @phpstan-ignore method.deprecated */ + $this->setUserConfiguration($config); + } + + /** + * Apply default sort order: Publication Date, newest first (9->1). + * Only sets it once; the user's explicit choice via the FreshRSS UI + * is preserved by checking the extension's own 'sort_applied' flag. + * + * @throws Minz_PermissionDeniedException + */ + private function applyDefaultSort(): void { + if ($this->cfgValue('sort_applied') === true) { + return; + } + + try { + $userConf = FreshRSS_Context::userConf(); + $userConf->_attribute('sort', self::DEFAULT_SORT); + $userConf->_attribute('sort_order', self::DEFAULT_SORT_ORDER); + $userConf->save(); + + // Mark as applied so we don't override the user's future changes + $config = $this->cfgAll(); + $config['sort_applied'] = true; + $this->cfgSave($config); + } catch (\Throwable $e) { + Minz_Log::warning('GridView: Failed to set default sort order: ' . $e->getMessage()); + } + } + + /** + * Inject JavaScript variables for grid configuration + * @param array $vars + * @return array + */ + public function injectJsVars(array $vars): array { + $columnsValue = $this->cfgValue('columns'); + $columns = is_numeric($columnsValue) ? (int) $columnsValue : self::DEFAULT_COLUMNS; + if ($columns < self::MIN_COLUMNS || $columns > self::MAX_COLUMNS) { + $columns = self::DEFAULT_COLUMNS; + } + $vars['gridview'] = [ + 'columns' => $columns, + 'placeholderUrl' => $this->getFileUrl('placeholder.jpg'), + 'ogFetchUrl' => './?c=extension&a=configure&e=' . urlencode($this->getName()), + 'showMobileMenuButton' => $this->isMobileMenuButtonEnabled(), + 'stickyNavEnabled' => $this->isStickyNavEnabled(), + 'shareMenuHtml' => $this->buildShareMenuHtml(), + ]; + return $vars; + } + + /** + * Build the share menu HTML template with placeholders. + * Reader view (article.phtml) doesn't render entry_share_menu, so + * we generate it here and pass it to JS for use in grid cards. + * Placeholders: --entryId--, --link--, --titleText-- + * @return string + */ + private function buildShareMenuHtml(): string { + if (!FreshRSS_Auth::hasAccess()) { + return ''; + } + + try { + $sharing = FreshRSS_Context::userConf()->sharing; + if (empty($sharing) || !is_array($sharing)) { + return ''; + } + } catch (\Throwable $e) { + return ''; + } + + $html = ''; + return $html; + } + + /** + * Handle configuration form submission + */ + #[\Override] + public function handleConfigureAction(): void { + // Handle AJAX OG image fetch request (returns JSON and exits) + if (Minz_Request::paramString('ajax') === 'og') { + $this->ajaxFetchOgImage(); + return; + } + + parent::init(); + + $this->registerTranslates(); + + if (Minz_Request::isPost()) { + $columns = Minz_Request::paramInt('columns'); + + // Validate column count + if ($columns < self::MIN_COLUMNS) { + $columns = self::MIN_COLUMNS; + } elseif ($columns > self::MAX_COLUMNS) { + $columns = self::MAX_COLUMNS; + } + + $fetchOgImage = Minz_Request::paramBoolean('fetch_og_image'); + $sortByDate = Minz_Request::paramBoolean('sort_by_date'); + $mobileMenuButton = Minz_Request::paramBoolean('mobile_menu_button'); + $stickyNav = Minz_Request::paramBoolean('sticky_nav'); + + // Reset sort_applied flag when sort_by_date is toggled so it + // re-applies (or stops applying) on the next page load + $sortApplied = $this->cfgValue('sort_applied') === true; + + // setUserConfiguration expects an array of all config values + $this->cfgSave([ + 'columns' => $columns, + 'fetch_og_image' => $fetchOgImage, + 'sort_by_date' => $sortByDate, + 'sort_applied' => $sortByDate ? $sortApplied : false, + 'mobile_menu_button' => $mobileMenuButton, + 'sticky_nav' => $stickyNav, + ]); + } + } + + /** + * Get current column configuration + * @return int + */ + public function getColumns(): int { + $value = $this->cfgValue('columns'); + $columns = is_numeric($value) ? (int) $value : self::DEFAULT_COLUMNS; + if ($columns < self::MIN_COLUMNS || $columns > self::MAX_COLUMNS) { + return self::DEFAULT_COLUMNS; + } + return $columns; + } + + /** + * Whether sort by publication date is enabled in user configuration. + * @return bool + */ + public function isSortByPublicationDateEnabled(): bool { + $value = $this->cfgValue('sort_by_date'); + return $value === true || $value === 1 || $value === '1'; + } + + /** + * Whether the mobile menu button is enabled in user configuration. + * @return bool + */ + public function isMobileMenuButtonEnabled(): bool { + $value = $this->cfgValue('mobile_menu_button'); + return $value === true || $value === 1 || $value === '1'; + } + + /** + * Whether the sticky navigation bar is enabled in user configuration. + * @return bool + */ + public function isStickyNavEnabled(): bool { + $value = $this->cfgValue('sticky_nav'); + return $value === true || $value === 1 || $value === '1'; + } + + /** + * Whether OG image fetching is enabled in user configuration. + * @return bool + */ + public function isOgImageFetchEnabled(): bool { + // Default to false (opt-in) so existing users aren't surprised by slower page loads + $value = $this->cfgValue('fetch_og_image'); + return $value === true || $value === 1 || $value === '1'; + } + + /** + * Inject a hidden thumbnail marker into the entry content. + * Reading View doesn't render the .item.thumbnail element, so the + * JS grid code can't find the thumbnail. This adds a hidden span + * with the URL so getThumbnail() can pick it up in any view. + */ + public function injectThumbnailMarker(FreshRSS_Entry $entry): FreshRSS_Entry { + $thumbnail = $entry->thumbnail(true); + if (empty($thumbnail['url'])) { + return $entry; + } + + $url = htmlspecialchars($thumbnail['url'], ENT_QUOTES, 'UTF-8'); + $marker = ''; + $entry->_content($marker . $entry->content(false)); + + return $entry; + } + + /** + * Fetch Open Graph image for entries that have no thumbnail. + * Called when a new entry is being inserted into the database. + * + * @throws Minz_PermissionDeniedException + */ + public function fetchOgImage(FreshRSS_Entry $entry): FreshRSS_Entry { + return $this->ensureThumbnail($entry); + } + + /** + * Mark entries that have no thumbnail so the JS can fetch OG images + * asynchronously. This replaces the synchronous fetchOgImageOnDisplay + * to avoid blocking page rendering. + */ + public function markEntryForOgFetch(FreshRSS_Entry $entry): FreshRSS_Entry { + $thumbnail = $entry->thumbnail(true); + if (!empty($thumbnail['url']) || $entry->attributeBoolean('og_image_checked')) { + return $entry; + } + + // Don't mark if the content already has images (the JS card + // builder will extract them directly) + if (preg_match('/]*src\s*=\s*["\']https?:\/\//i', $entry->content(false))) { + return $entry; + } + + $link = $entry->link(); + if (empty($link) || !str_starts_with($link, 'http')) { + return $entry; + } + + $escapedLink = htmlspecialchars($link, ENT_QUOTES, 'UTF-8'); + $escapedId = htmlspecialchars($entry->id(), ENT_QUOTES, 'UTF-8'); + + $marker = ''; + $entry->_content($entry->content(false) . $marker); + + return $entry; + } + + /** + * AJAX endpoint: fetch the OG image for a given URL and return JSON. + * Called via ?c=extension&a=configure&e=GridView&ajax=og&url=... + */ + private function ajaxFetchOgImage(): void { + header('Content-Type: application/json; charset=utf-8'); + header('Cache-Control: private, max-age=86400'); + + if (!FreshRSS_Auth::hasAccess()) { + http_response_code(403); + echo json_encode(['error' => 'Unauthorized']); + exit; + } + + $url = Minz_Request::paramString('url'); + if (empty($url) || (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://'))) { + http_response_code(400); + echo json_encode(['error' => 'Invalid URL']); + exit; + } + + $ogImage = $this->extractOgImage($url); + + echo json_encode(['image' => $ogImage]); + exit; + } + + /** + * Ensure the entry has a thumbnail; if not, try to fetch the OG image. + * + * @param FreshRSS_Entry $entry + * @param bool $persist Whether to persist changes to the database (for display-time fetches) + * @return FreshRSS_Entry + * @throws Minz_PermissionDeniedException + */ + private function ensureThumbnail(FreshRSS_Entry $entry, bool $persist = false): FreshRSS_Entry { + // Skip if entry already has a thumbnail + $thumbnail = $entry->thumbnail(true); + if (!empty($thumbnail['url'])) { + return $entry; + } + + // Don't re-attempt if we already checked this entry + if ($entry->attributeBoolean('og_image_checked')) { + return $entry; + } + + // Skip if content already has images + if (preg_match('/]*src\s*=\s*["\']https?:\/\//i', $entry->content(false))) { + return $entry; + } + + $link = htmlspecialchars_decode($entry->link(), ENT_QUOTES); + if (empty($link) || !str_starts_with($link, 'http')) { + return $entry; + } + + $ogImage = $this->extractOgImage($link); + if ($ogImage !== null) { + $entry->_attribute('thumbnail', ['url' => $ogImage]); + } else { + $entry->_attribute('og_image_checked', true); + } + + // Persist to database when called at display time so the image + // is available on subsequent page loads without re-fetching + if ($persist) { + try { + $entryDAO = FreshRSS_Factory::createEntryDao(); + $entryDAO->updateEntry($entry->toArray()); + } catch (\Throwable $e) { + Minz_Log::warning('GridView: Failed to persist OG image: ' . $e->getMessage()); + } + } + + return $entry; + } + + /** + * Fetch a URL and extract the og:image meta tag. + * + * @param string $url The article URL to fetch + * @return string|null The og:image URL or null if not found + */ + private function extractOgImage(string $url): ?string { + $ctx = stream_context_create([ + 'http' => [ + 'timeout' => 5, + 'method' => 'GET', + 'header' => "User-Agent: FreshRSS/GridView\r\n", + 'follow_location' => 1, + 'max_redirects' => 3, + 'ignore_errors' => true, + ], + 'ssl' => [ + 'verify_peer' => true, + 'verify_peer_name' => true, + ], + ]); + + // Only fetch the first 50 KB — og:image is in + $html = @file_get_contents($url, false, $ctx, 0, 50000); + if ($html === false || $html === '') { + return null; + } + + // Try og:image first, then twitter:image (both attribute orderings) + if (preg_match('/]*property=["\']og:image["\'][^>]*content=["\']([^"\']+)["\']/i', $html, $matches)) { + return $this->normalizeImageUrl($matches[1], $url); + } + if (preg_match('/]*content=["\']([^"\']+)["\'][^>]*property=["\']og:image["\']/i', $html, $matches)) { + return $this->normalizeImageUrl($matches[1], $url); + } + if (preg_match('/]*name=["\']twitter:image["\'][^>]*content=["\']([^"\']+)["\']/i', $html, $matches)) { + return $this->normalizeImageUrl($matches[1], $url); + } + if (preg_match('/]*content=["\']([^"\']+)["\'][^>]*name=["\']twitter:image["\']/i', $html, $matches)) { + return $this->normalizeImageUrl($matches[1], $url); + } + + return null; + } + + /** + * Normalize a potentially relative image URL to absolute. + * + * @param string $imageUrl The image URL (may be relative) + * @param string $pageUrl The page URL for resolving relative paths + * @return string|null Absolute URL or null if invalid + */ + private function normalizeImageUrl(string $imageUrl, string $pageUrl): ?string { + $imageUrl = html_entity_decode($imageUrl, ENT_QUOTES, 'UTF-8'); + $imageUrl = trim($imageUrl); + + if (empty($imageUrl)) { + return null; + } + + // Already absolute + if (str_starts_with($imageUrl, 'http://') || str_starts_with($imageUrl, 'https://')) { + return $imageUrl; + } + + // Protocol-relative + if (str_starts_with($imageUrl, '//')) { + $scheme = parse_url($pageUrl, PHP_URL_SCHEME) ?: 'https'; + return $scheme . ':' . $imageUrl; + } + + // Relative — resolve against page URL + $parsed = parse_url($pageUrl); + if ($parsed === false || empty($parsed['host'])) { + return null; + } + + $base = ($parsed['scheme'] ?? 'https') . '://' . $parsed['host']; + if (str_starts_with($imageUrl, '/')) { + return $base . $imageUrl; + } + + $path = $parsed['path'] ?? '/'; + $dir = substr($path, 0, (int)strrpos($path, '/') + 1); + return $base . $dir . $imageUrl; + } +} diff --git a/xExtension-GridView/i18n/de/ext.php b/xExtension-GridView/i18n/de/ext.php new file mode 100644 index 0000000..7884306 --- /dev/null +++ b/xExtension-GridView/i18n/de/ext.php @@ -0,0 +1,26 @@ + array( + 'view_mode_name' => 'Rasteransicht', + 'config' => array( + 'columns' => 'Anzahl der Spalten', + 'columns_label' => 'Spalten', + 'columns_help' => 'Wählen Sie aus, wie viele Spalten in der Rasteransicht angezeigt werden sollen (2-4). Auf kleineren Bildschirmen passt sich dies automatisch an.', + 'fetch_og_image' => 'Vorschaubilder abrufen', + 'fetch_og_image_label' => 'Vorschaubilder von Artikelseiten abrufen', + 'fetch_og_image_help' => 'Wenn aktiviert, ruft die Erweiterung Open-Graph-Bilder von Artikelseiten ab, die kein Vorschaubild im RSS-Feed haben. Dies verbessert die Kartenbilder, kann aber das Laden der Seite verlangsamen.', + 'sort_by_date' => 'Standard-Sortierung', + 'sort_by_date_label' => 'Nach Veröffentlichungsdatum sortieren (neueste zuerst)', + 'sort_by_date_help' => 'Wenn aktiviert, wird die Standard-Sortierung auf Veröffentlichungsdatum (neueste zuerst) gesetzt. Dies wird einmalig angewendet; Sie können die Sortierung in FreshRSS weiterhin manuell ändern.', + 'mobile_menu_button' => 'Seitenleisten-Umschalttaste', + 'mobile_menu_button_label' => 'Schwebende Seitenleisten-Umschalttaste anzeigen', + 'mobile_menu_button_help' => 'Wenn aktiviert, erscheint unten links auf dem Bildschirm eine schwebende Hamburger-Taste, um die FreshRSS-Seitenleiste zu öffnen.', + 'sticky_nav' => 'Feste Navigationsleiste', + 'sticky_nav_label' => 'Navigationsleiste beim Scrollen sichtbar halten', + 'sticky_nav_help' => 'Wenn aktiviert, bleibt die Navigationsleiste (mit gelesen/ungelesen, Favoriten usw.) beim Scrollen durch Artikel im Rastermodus oben fixiert.', + 'usage_title' => 'Verwendung', + 'usage_info' => 'Nach dem Speichern Ihrer Einstellungen klicken Sie auf das Rastersymbol (▦) in der Kopfzeile oder drücken Sie „G" auf Ihrer Tastatur, um die Rasteransicht umzuschalten. Klicken Sie auf eine Karte, um den Artikel in einem neuen Tab zu öffnen.', + ), + ), +); diff --git a/xExtension-GridView/i18n/en/ext.php b/xExtension-GridView/i18n/en/ext.php new file mode 100644 index 0000000..ab8dbb0 --- /dev/null +++ b/xExtension-GridView/i18n/en/ext.php @@ -0,0 +1,26 @@ + array( + 'view_mode_name' => 'Grid View', + 'config' => array( + 'columns' => 'Number of columns', + 'columns_label' => 'columns', + 'columns_help' => 'Choose how many columns to display in the grid view (2-4). On smaller screens, this will automatically adjust.', + 'fetch_og_image' => 'Thumbnail fetching', + 'fetch_og_image_label' => 'Fetch thumbnails from article pages', + 'fetch_og_image_help' => 'When enabled, the extension will fetch Open Graph images from article pages that have no thumbnail in the RSS feed. This provides better card images but may slow down page loading.', + 'sort_by_date' => 'Default sorting', + 'sort_by_date_label' => 'Sort by publication date (newest first)', + 'sort_by_date_help' => 'When enabled, the default sort order is set to publication date, newest first. This is applied once; you can still change the sort order manually in FreshRSS.', + 'mobile_menu_button' => 'Sidebar toggle button', + 'mobile_menu_button_label' => 'Show a floating sidebar toggle button', + 'mobile_menu_button_help' => 'When enabled, a floating hamburger button appears at the bottom-left of the screen to open the FreshRSS sidebar.', + 'sticky_nav' => 'Sticky navigation bar', + 'sticky_nav_label' => 'Keep the top navigation bar visible while scrolling', + 'sticky_nav_help' => 'When enabled, the navigation bar (with read/unread, favourites, etc.) stays fixed at the top while scrolling through articles in grid mode.', + 'usage_title' => 'How to use', + 'usage_info' => 'After saving your settings, click the grid icon (▦) in the header or press "G" on your keyboard to toggle grid view. Click on any card to open the article in a new tab.', + ), + ), +); diff --git a/xExtension-GridView/i18n/fr/ext.php b/xExtension-GridView/i18n/fr/ext.php new file mode 100644 index 0000000..2412e54 --- /dev/null +++ b/xExtension-GridView/i18n/fr/ext.php @@ -0,0 +1,26 @@ + array( + 'view_mode_name' => 'Vue Grille', + 'config' => array( + 'columns' => 'Nombre de colonnes', + 'columns_label' => 'colonnes', + 'columns_help' => 'Choisissez le nombre de colonnes à afficher dans la vue grille (2-4). Sur les petits écrans, cela s\'ajustera automatiquement.', + 'fetch_og_image' => 'Récupération des miniatures', + 'fetch_og_image_label' => 'Récupérer les miniatures depuis les pages des articles', + 'fetch_og_image_help' => 'Lorsque cette option est activée, l\'extension récupère les images Open Graph des pages d\'articles qui n\'ont pas de miniature dans le flux RSS. Cela améliore les images des cartes mais peut ralentir le chargement des pages.', + 'sort_by_date' => 'Tri par défaut', + 'sort_by_date_label' => 'Trier par date de publication (plus récent en premier)', + 'sort_by_date_help' => 'Lorsque cette option est activée, l\'ordre de tri par défaut est défini sur la date de publication, du plus récent au plus ancien. Ceci est appliqué une seule fois ; vous pouvez toujours modifier l\'ordre de tri manuellement dans FreshRSS.', + 'mobile_menu_button' => 'Bouton du panneau latéral', + 'mobile_menu_button_label' => 'Afficher un bouton flottant pour le panneau latéral', + 'mobile_menu_button_help' => 'Lorsque cette option est activée, un bouton hamburger flottant apparaît en bas à gauche de l\'écran pour ouvrir le panneau latéral de FreshRSS.', + 'sticky_nav' => 'Barre de navigation fixe', + 'sticky_nav_label' => 'Garder la barre de navigation visible lors du défilement', + 'sticky_nav_help' => 'Lorsque cette option est activée, la barre de navigation (avec lu/non lu, favoris, etc.) reste fixée en haut lors du défilement des articles en mode grille.', + 'usage_title' => 'Comment utiliser', + 'usage_info' => 'Après avoir enregistré vos paramètres, cliquez sur l\'icône grille (▦) dans l\'en-tête ou appuyez sur « G » sur votre clavier pour basculer la vue grille. Cliquez sur n\'importe quelle carte pour ouvrir l\'article dans un nouvel onglet.', + ), + ), +); diff --git a/xExtension-GridView/metadata.json b/xExtension-GridView/metadata.json new file mode 100644 index 0000000..73eabd6 --- /dev/null +++ b/xExtension-GridView/metadata.json @@ -0,0 +1,8 @@ +{ + "name": "Grid View", + "author": "Marius Stroe ", + "description": "A grid/card view for displaying feeds with images, configurable 2-4 columns, FreshRSS native share integration, and browser reader mode support", + "version": "1.1.0", + "entrypoint": "GridView", + "type": "user" +} diff --git a/xExtension-GridView/static/grid.css b/xExtension-GridView/static/grid.css new file mode 100644 index 0000000..a0137e5 --- /dev/null +++ b/xExtension-GridView/static/grid.css @@ -0,0 +1,781 @@ +/** + * Grid View Extension - CSS Styles + * Card layout for FreshRSS + */ + +/* ============================================ + Grid Toggle & Refresh Buttons + ============================================ */ + +.read_grid, +[data-grid-toggle] { + padding: 0.25rem 0.5rem; + display: inline-flex; + font-size: 1.25rem; + border: 1px solid var(--frss-border-color, #ddd); + border-radius: 4px; + align-items: center; + justify-content: center; + margin-left: 0.5rem; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; + text-decoration: none; +} + +.read_grid:hover, +[data-grid-toggle]:hover { + background-color: var(--frss-background-alt, #f0f0f0); +} + +.read_grid.active, +[data-grid-toggle].active { + background-color: var(--frss-accent-color, #4a90d9) !important; + color: #fff !important; + border-color: var(--frss-accent-color, #4a90d9); +} + +/* ============================================ + Grid Container - Applied when view mode is 'grid' + ============================================ */ + +#stream.grid { + padding: 1.25rem; + display: grid; + max-width: 100%; + grid-template-columns: repeat(var(--gridview-columns, 3), 1fr); + gap: 1.25rem; + box-sizing: border-box; +} + +/* ============================================ + Date Separators / Day Dividers + ============================================ */ + +#stream.grid > .gridview-header { + margin: 0 0 0.25rem 0; + padding: 0.75rem 0; + display: flex; + width: 100%; + color: var(--frss-text-color, #333); + font-size: 1.25rem; + border-bottom: 2px solid var(--frss-accent-color, #4a90d9); + grid-column: 1 / -1; + font-weight: 700; + align-items: baseline; + gap: 0.75rem; +} + +.gridview-header-name { + flex-shrink: 0; +} + +.gridview-header-new-articles { + display: none; + color: var(--frss-accent-color, #4a90d9); + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + white-space: nowrap; +} + +.gridview-header-new-articles.visible { + display: inline; +} + +.gridview-header-new-articles:hover { + text-decoration: underline; +} + +/* Hide the native FreshRSS new-article banner in grid mode */ +#stream.grid > #new-article, +body.gridview-active #new-article { + display: none !important; +} + +#stream.grid > .transition { + display: none; +} + +#stream.grid .day, +#stream.grid .date-separator, +#stream.grid .flux_date, +#stream.grid > .day-title, +#stream.grid > h2, +#stream.grid > h3 { + margin: 0.5rem 0; + padding: 0.75rem 0; + background: transparent; + display: flex; + width: 100%; + color: var(--frss-text-muted, #666); + font-size: 0.875rem; + border-bottom: 1px solid var(--frss-border-color, #e0e0e0); + grid-column: 1 / -1; /* Span full width */ + font-weight: 600; + align-items: center; +} + +#stream.grid .day::before, +#stream.grid .date-separator::before { + content: "📅 "; + margin-right: 0.5rem; +} + +/* ============================================ + Card Styling + ============================================ */ + +#stream.grid .flux { + margin: 0; + /* Reset padding/margin that FreshRSS Reader view may add to .flux */ + padding: 0; + background: var(--frss-background-color, #fff); + display: flex; + height: 100%; + border: 1px solid var(--frss-border-color, #e0e0e0); + border-radius: 12px; + flex-direction: column; + overflow: hidden; + isolation: isolate; + transition: box-shadow 0.2s ease, border 0.2s ease; + cursor: pointer; + min-height: 482px; +} + +#stream.grid .flux:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +/* Override FreshRSS default .not_read border in grid mode. + The actual unread indicator is managed via inline styles + by syncFluxVisualState() in grid.js. */ + +#stream.grid .flux.not_read { + border-left: 1px solid var(--frss-border-color, #e0e0e0); +} + +/* Override .current styling in grid mode. FreshRSS sets .current on the + most recently interacted article; the box-shadow looked like an unwanted + full blue border when switching from list view to grid view. */ + +#stream.grid .flux.current { + box-shadow: none; +} + +#stream.grid .flux.current:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +/* ============================================ + Card Image / Thumbnail + ============================================ */ + +#stream.grid .flux .flux_header { + margin: 0; + padding: 0; + background: none; + display: flex; + border: none; + flex-direction: column; + position: relative; +} + +#stream.grid .flux .card-thumbnail { + background: var(--frss-background-alt, #f5f5f5); + display: flex; + width: 100%; + height: 394px; + border-radius: inherit; + overflow: hidden; + align-items: center; + justify-content: center; + cursor: pointer; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + will-change: transform; +} + +#stream.grid .flux .card-thumbnail img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +#stream.grid .flux:hover .card-thumbnail img { + transform: scale(1.05); +} + +#stream.grid .flux .card-thumbnail .no-image { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + width: 100%; + height: 100%; + color: #fff; + font-size: 3rem; + align-items: center; + justify-content: center; + opacity: 0.7; +} + +#stream.grid .flux .card-thumbnail .placeholder-image { + width: 100%; + height: 100%; + object-fit: cover; + filter: brightness(0.85) saturate(0.9); +} + +#stream.grid .flux:hover .card-thumbnail .placeholder-image { + filter: brightness(0.9) saturate(1); +} + +/* ============================================ + Card Content Area + ============================================ */ + +#stream.grid .flux .card-content { + padding: 1rem; + display: flex; + flex-direction: column; + flex-grow: 1; + position: relative; +} + +#stream.grid .flux .card-meta { + display: flex; + color: var(--frss-text-muted, #666); + font-size: 0.75rem; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +#stream.grid .flux .card-meta .favicon { + width: 16px; + height: 16px; + border-radius: 2px; +} + +#stream.grid .flux .card-meta .feed-name { + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 60%; +} + +#stream.grid .flux .card-meta .reading-time { + margin-left: auto; + white-space: nowrap; +} + +#stream.grid .flux .card-title { + margin: 0 0 0.5rem 0; + color: var(--frss-text-color, #333); + font-size: 1rem; + font-weight: 600; + line-height: 1.4; + overflow-wrap: break-word; +} + +#stream.grid .flux .card-title a { + color: inherit; + text-decoration: none; +} + +#stream.grid .flux .card-title a:hover { + color: var(--frss-accent-color, #4a90d9); +} + +#stream.grid .flux .card-summary-wrapper { + position: relative; + flex-grow: 1; + margin: 0; + cursor: pointer; +} + +#stream.grid .flux .card-summary { + margin: 0; + color: var(--frss-text-muted, #666); + font-size: 0.875rem; + line-height: 1.5; + overflow: hidden; +} + +/* Scrollable tooltip panel shown on hover — fills .card-content area */ +#stream.grid .flux .card-summary-tooltip { + padding: 0.75rem 1rem; + background: var(--frss-background-color, #fff); + display: none; + border-radius: 0; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow-y: auto; + z-index: 100; +} + +#stream.grid .flux .card-summary-tooltip p { + margin: 0; + color: var(--frss-text-color, #333); + font-size: 0.875rem; + line-height: 1.5; + padding-bottom: 1rem; +} + +#stream.grid .flux .card-summary-tooltip.expanded { + display: block; +} + +#stream.grid .flux .card-date { + color: var(--frss-text-muted, #888); + font-size: 0.75rem; + margin-top: auto; + padding-top: 0.75rem; +} + +/* ============================================ + Hide default elements in grid view + ============================================ */ + +#stream.grid .flux .flux_content, +#stream.grid .flux .item.manage, +#stream.grid .flux .item.link, +#stream.grid .flux > .flux_header > .horizontal-list { + display: none !important; +} + +/* Hide the FreshRSS prev/up/next arrow buttons in grid mode */ +body.gridview-active #nav_entries { + display: none !important; +} + +/* Ensure grid card wrapper fills the flux without extra spacing + (Reader view may apply padding/margin to .flux children) */ + +#stream.grid .flux .gridview-card-content { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +/* ============================================ + Card Actions (on hover) + ============================================ */ + +#stream.grid .flux .card-actions { + padding: 0.75rem 1rem; + background: rgba(0, 0, 0, 0.55); + display: flex; + border-top: none; + gap: 0.5rem; + position: absolute; + bottom: 0; + left: 0; + right: 0; + backdrop-filter: blur(4px); + opacity: 0; + transition: opacity 0.2s ease; + z-index: 10; +} + +#stream.grid .flux.card-selected .card-actions { + opacity: 1; +} + +#stream.grid .flux .card-actions button, +#stream.grid .flux .card-actions a { + padding: 0.25rem 0.5rem; + background: none; + color: rgba(255, 255, 255, 0.9); + font-size: 0.75rem; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; +} + +#stream.grid .flux .card-actions button:hover, +#stream.grid .flux .card-actions a:hover { + background: rgba(255, 255, 255, 0.2); + color: #fff; +} + +#stream.grid .flux.favorite .card-actions .action-star { + color: #f5a623; +} + +/* ============================================ + Share Button in Cards + ============================================ */ + +#stream.grid .flux .card-actions .action-share { + padding: 0.25rem 0.5rem; + background: none; + display: inline-flex; + color: rgba(255, 255, 255, 0.9); + font-size: 0.85rem; + border: none; + border-radius: 4px; + align-items: center; + justify-content: center; + cursor: pointer; +} + +#stream.grid .flux .card-actions .action-share:hover { + background: rgba(255, 255, 255, 0.2); + color: #fff; +} + +#stream.grid .flux .card-actions .action-share .icon { + width: 16px; + height: 16px; + opacity: 0.7; +} + +#stream.grid .flux .card-actions .action-share:hover .icon { + opacity: 1; + filter: brightness(0) invert(1); +} + +/* ============================================ + Hidden Original Content (for FreshRSS functionality) + ============================================ */ + +#stream.grid .flux .gridview-original-content { + display: none !important; +} + +/* But show the share dropdown when it's activated via :target */ +#stream.grid .flux .gridview-original-content .dropdown-menu, +#stream.grid .flux .gridview-original-content .item.share .dropdown-menu { + display: none; +} + +/* When dropdown is targeted, show it positioned relative to the card */ +#stream.grid .flux .gridview-original-content .dropdown-target:target ~ .dropdown-menu { + padding: 0.5rem 0; + background: var(--frss-background-color, #fff); + display: block !important; + border: 1px solid var(--frss-border-color, #ddd); + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + position: fixed; + z-index: 9999; + min-width: 180px; +} + +/* ============================================ + Prevent Flash of List View (FOLV) + Hide untransformed flux entries in grid mode + so the default list layout never shows. + ============================================ */ + +#stream.grid .flux:not(.gridview-transformed) { + margin: 0; + padding: 0; + height: 0; + border: none; + visibility: hidden; + min-height: 0; + overflow: hidden; +} + +/* When grid mode is active (body flag), hide the raw stream content + even before JS adds .grid to a newly replaced #stream element. + This prevents the brief flash of list view during AJAX navigation. */ + +body.gridview-active #stream:not(.grid) { + visibility: hidden; + min-height: 200px; +} + +/* ============================================ + Loading Skeleton / Spinner + Shown while grid items are being transformed + ============================================ */ + +.gridview-loading-overlay { + padding: 0; + display: flex; + width: 100%; + grid-column: 1 / -1; + flex-wrap: wrap; + gap: 1.25rem; +} + +.gridview-skeleton-card { + background: var(--frss-background-color, #fff); + max-width: calc(100% / var(--gridview-columns, 3) - 1.25rem + 1.25rem / var(--gridview-columns, 3)); + border: 1px solid var(--frss-border-color, #e0e0e0); + border-radius: 12px; + flex: 1 1 calc(100% / var(--gridview-columns, 3) - 1.25rem); + min-width: 200px; + overflow: hidden; + min-height: 482px; +} + +.gridview-skeleton-card .skeleton-thumbnail { + background: linear-gradient(90deg, + var(--frss-background-alt, #f0f0f0) 25%, + var(--frss-border-color, #e8e8e8) 50%, + var(--frss-background-alt, #f0f0f0) 75%); + width: 100%; + height: 394px; + background-size: 200% 100%; + animation: gridview-shimmer 1.5s infinite; +} + +.gridview-skeleton-card .skeleton-content { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.625rem; +} + +.gridview-skeleton-card .skeleton-line { + background: linear-gradient(90deg, + var(--frss-background-alt, #f0f0f0) 25%, + var(--frss-border-color, #e8e8e8) 50%, + var(--frss-background-alt, #f0f0f0) 75%); + height: 0.875rem; + border-radius: 4px; + background-size: 200% 100%; + animation: gridview-shimmer 1.5s infinite; +} + +.gridview-skeleton-card .skeleton-line.short { + width: 40%; +} + +.gridview-skeleton-card .skeleton-line.medium { + width: 75%; +} + +.gridview-skeleton-card .skeleton-line.long { + width: 100%; +} + +@keyframes gridview-shimmer { + 0% { background-position: 200% 0; } + + 100% { background-position: -200% 0; } +} + +/* ============================================ + Toast Notification + ============================================ */ + +@keyframes gridview-toast-fade { + 0% { opacity: 0; transform: translateX(-50%) translateY(10px); } + + 10% { opacity: 1; transform: translateX(-50%) translateY(0); } + + 90% { opacity: 1; transform: translateX(-50%) translateY(0); } + + 100% { opacity: 0; transform: translateX(-50%) translateY(-10px); } +} + +/* ============================================ + Mobile Bottom Bar (Menu Button) + ============================================ */ + +.gridview-mobile-bar { + padding: 0.5rem; + display: none; + position: fixed; + bottom: 0; + left: 0; + z-index: 1000; + padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px)); +} + +body.gridview-active .gridview-mobile-bar { + display: block; +} + +.gridview-mobile-menu-btn { + background: var(--frss-background-color, #fff); + display: flex; + width: 2.75rem; + height: 2.75rem; + color: var(--frss-text-color, #333); + border: 1px solid var(--frss-border-color, #ddd); + border-radius: 50%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + align-items: center; + justify-content: center; + cursor: pointer; + transition: background-color 0.2s ease, box-shadow 0.2s ease; + -webkit-tap-highlight-color: transparent; +} + +.gridview-mobile-menu-btn:active { + background: var(--frss-background-alt, #f0f0f0); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); +} + + +/* ============================================ + Sticky Nav Bar in Grid Mode + Keep the FreshRSS toolbar visible while scrolling + in grid mode (Normal and Reader views). + + FreshRSS uses display:table on #global with .aside as + a table-cell. position:sticky cannot work inside a table + layout, so we: + 1. Switch #global from table to flex layout + 2. Wrap .nav_menu + #stream in a scroll container via JS + (see grid.js: wrapContentForStickyNav) + 3. Make .nav_menu sticky inside that scroll container + ============================================ */ + +body.gridview-sticky-nav #global { + display: flex; + flex-direction: row; +} + +body.gridview-sticky-nav #global > .aside { + display: block; + flex-shrink: 0; +} + +body.gridview-sticky-nav .gridview-scroll-wrapper { + flex: 1 1 0%; + min-width: 0; + overflow-y: auto; +} + +body.gridview-sticky-nav .nav_menu { + position: sticky; + top: 0; + z-index: 50; + background: var(--frss-background-color, #fff); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +@media (prefers-color-scheme: dark) { + body.gridview-sticky-nav .nav_menu { + background: var(--frss-background-color, #1e1e1e); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + } +} + +.theme-dark body.gridview-sticky-nav .nav_menu, +body.theme-dark.gridview-sticky-nav .nav_menu, +body.gridview-sticky-nav .theme-dark .nav_menu, +[data-theme="dark"] body.gridview-sticky-nav .nav_menu, +body.gridview-sticky-nav[data-theme="dark"] .nav_menu { + background: var(--frss-background-color, #1e1e1e); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} + +/* ============================================ + Responsive Breakpoints + ============================================ */ + +@media (max-width: 1200px) { + #stream.grid { + grid-template-columns: repeat(min(var(--gridview-columns, 3), 3), 1fr); + } +} + +@media (max-width: 900px) { + #stream.grid { + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + padding: 1rem; + } +} + +@media (max-width: 600px) { + #stream.grid { + grid-template-columns: 1fr; + gap: 0.75rem; + padding: 0.75rem; + } + + #stream.grid .flux { + min-height: 419px; + } + + #stream.grid .flux .card-thumbnail { + height: 328px; + } +} + +/* ============================================ + Dark Theme Support + ============================================ */ + +@media (prefers-color-scheme: dark) { + #stream.grid .flux { + background: var(--frss-background-color, #1e1e1e); + border-color: var(--frss-border-color, #333); + } + + #stream.grid .flux:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + + #stream.grid .flux .card-thumbnail .no-image { + background: linear-gradient(135deg, #434343 0%, #000 100%); + } + + #stream.grid .flux .card-summary-tooltip { + background: var(--frss-background-color, #1e1e1e); + border-color: var(--frss-border-color, #333); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); + } +} + +/* Support FreshRSS dark themes */ +.theme-dark #stream.grid .flux, +[data-theme="dark"] #stream.grid .flux { + background: var(--frss-background-color, #1e1e1e); + border-color: var(--frss-border-color, #333); +} + +.theme-dark #stream.grid .flux:hover, +[data-theme="dark"] #stream.grid .flux:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.theme-dark #stream.grid .flux .card-summary-tooltip, +[data-theme="dark"] #stream.grid .flux .card-summary-tooltip { + background: var(--frss-background-color, #1e1e1e); + border-color: var(--frss-border-color, #333); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); +} + +@media (prefers-color-scheme: dark) { + .gridview-mobile-menu-btn { + background: var(--frss-background-color, #1e1e1e); + color: var(--frss-text-color, #e0e0e0); + border-color: var(--frss-border-color, #333); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + } +} + +@media (prefers-color-scheme: dark) { + .gridview-skeleton-card { + background: var(--frss-background-color, #1e1e1e); + border-color: var(--frss-border-color, #333); + } +} + +.theme-dark .gridview-mobile-menu-btn, +[data-theme="dark"] .gridview-mobile-menu-btn { + background: var(--frss-background-color, #1e1e1e); + color: var(--frss-text-color, #e0e0e0); + border-color: var(--frss-border-color, #333); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); +} + +.theme-dark .gridview-skeleton-card, +[data-theme="dark"] .gridview-skeleton-card { + background: var(--frss-background-color, #1e1e1e); + border-color: var(--frss-border-color, #333); +} diff --git a/xExtension-GridView/static/grid.js b/xExtension-GridView/static/grid.js new file mode 100644 index 0000000..b8f6a6e --- /dev/null +++ b/xExtension-GridView/static/grid.js @@ -0,0 +1,1974 @@ +/** + * Grid View Extension - JavaScript + * Transforms the default FreshRSS view into a card-based grid layout + */ + +(function () { + 'use strict'; + + const STORAGE_KEY = 'freshrss-gridview-enabled'; + let gridEnabled = false; + let columns = 3; + let placeholderUrl = ''; + let streamObserver = null; + let parentObserver = null; + let classObserver = null; + let initialized = false; + let ogFetchUrl = ''; + let showMobileMenuButton = false; + let shareMenuHtml = ''; + let stickyNavEnabled = false; + /** @type {function|null} Bound scroll handler for auto-load-more cleanup */ + let autoLoadScrollHandler = null; + + /** + * Create a loading overlay with skeleton cards to show while + * grid items are being transformed. + * @param {HTMLElement} stream + */ + function showLoadingSkeleton(stream) { + // Don't add duplicates + if (stream.querySelector('.gridview-loading-overlay')) return; + + const overlay = document.createElement('div'); + overlay.className = 'gridview-loading-overlay'; + + const count = (columns || 3) * 2; // two rows of skeleton cards + for (let i = 0; i < count; i++) { + const card = document.createElement('div'); + card.className = 'gridview-skeleton-card'; + card.innerHTML = + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; + overlay.appendChild(card); + } + + // Insert at top so it appears immediately + stream.prepend(overlay); + } + + /** + * Remove the loading overlay from the stream. + * @param {HTMLElement} stream + */ + function removeLoadingSkeleton(stream) { + const overlay = stream.querySelector('.gridview-loading-overlay'); + if (overlay) overlay.remove(); + } + + /** + * Initialize the grid view extension + */ + function initGridView() { + if (initialized) return; + initialized = true; + + // Get configuration from context.extensions (where js_vars hook puts data) + if (typeof window.context !== 'undefined' && window.context.extensions) { + if (window.context.extensions.gridview) { + columns = window.context.extensions.gridview.columns || 3; + gridEnabled = window.context.extensions.gridview.enabled || false; + placeholderUrl = window.context.extensions.gridview.placeholderUrl || ''; + ogFetchUrl = window.context.extensions.gridview.ogFetchUrl || ''; + showMobileMenuButton = window.context.extensions.gridview.showMobileMenuButton || false; + stickyNavEnabled = window.context.extensions.gridview.stickyNavEnabled || false; + shareMenuHtml = window.context.extensions.gridview.shareMenuHtml || ''; + } + } + + // Check localStorage for user preference (overrides server setting) + const storedPref = localStorage.getItem(STORAGE_KEY); + if (storedPref !== null) { + gridEnabled = storedPref === 'true'; + } + + // Set up toggle button + setupToggleButton(); + + // Apply grid view if enabled + if (gridEnabled) { + enableGridView(); + } + + // Deselect card when clicking/tapping outside + function deselectCard(e) { + if (!e.target.closest('.flux.card-selected')) { + const selected = document.querySelector('.flux.card-selected'); + if (selected) { + selected.classList.remove('card-selected'); + } + } + } + document.addEventListener('touchend', deselectCard); + document.addEventListener('click', deselectCard); + + // Watch for FreshRSS replacing the #stream element entirely + // (e.g. on feed navigation) so we can re-apply grid mode immediately. + observeStreamReplacement(); + } + + /** + * Set up the grid toggle button in the navigation + */ + function setupToggleButton() { + // Try to find or create the toggle button + let toggleBtn = document.querySelector('.read_grid, [data-grid-toggle]'); + + if (!toggleBtn) { + // Create toggle button and add to reading modes + const readingModes = document.querySelector('.reading_modes, .nav_menu .group-controls, #nav_menu_read_all'); + if (readingModes) { + toggleBtn = document.createElement('a'); + toggleBtn.href = '#'; + toggleBtn.className = 'read_grid btn'; + toggleBtn.setAttribute('data-grid-toggle', ''); + toggleBtn.title = 'Grid View'; + toggleBtn.innerHTML = ''; + readingModes.appendChild(toggleBtn); + } + } + + // Also try adding to the header actions area + if (!toggleBtn) { + const headerActions = document.querySelector('.header .group-controls, .nav_menu, #stream .flux_header'); + if (headerActions) { + toggleBtn = document.createElement('a'); + toggleBtn.href = '#'; + toggleBtn.className = 'read_grid btn'; + toggleBtn.setAttribute('data-grid-toggle', ''); + toggleBtn.title = 'Grid View'; + toggleBtn.innerHTML = '▦'; + + // Insert at beginning + headerActions.insertBefore(toggleBtn, headerActions.firstChild); + } + } + + if (toggleBtn) { + toggleBtn.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + toggleGridView(); + }); + + // Update button state + updateToggleButtonState(toggleBtn); + } + + // Add keyboard shortcut (g key) + document.addEventListener('keydown', function (e) { + // Don't trigger if typing in an input or editable element + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return; + + if (e.key === 'g' && !e.ctrlKey && !e.metaKey && !e.altKey) { + toggleGridView(); + } + }); + + // Set up mobile menu button (if enabled in extension settings) + if (showMobileMenuButton) { + setupMobileMenuButton(); + } + } + + /** + * Set up a fixed mobile bottom bar with a menu button that + * toggles the FreshRSS sidebar (#aside_feed). + */ + function setupMobileMenuButton() { + if (document.querySelector('.gridview-mobile-bar')) return; + + const bar = document.createElement('div'); + bar.className = 'gridview-mobile-bar'; + + const menuBtn = document.createElement('button'); + menuBtn.className = 'gridview-mobile-menu-btn'; + menuBtn.type = 'button'; + menuBtn.title = 'Open menu'; + menuBtn.setAttribute('aria-label', 'Open menu'); + // Hamburger icon (three horizontal lines) + menuBtn.innerHTML = '' + + '' + + '' + + '' + + ''; + + menuBtn.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + toggleAsideFeed(); + }); + + bar.appendChild(menuBtn); + document.body.appendChild(bar); + } + + /** + * Toggle the FreshRSS sidebar. + * Delegates to FreshRSS's own toggle_aside_click() when available, + * which adds/removes the .visible class on .aside and toggles + * display style. Falls back to clicking the native toggle button. + */ + function toggleAsideFeed() { + // Preferred: call FreshRSS's own toggle function + if (typeof window.toggle_aside_click === 'function') { + window.toggle_aside_click(true); + return; + } + + // Fallback: programmatically click FreshRSS's toggle button + const toggleBtn = document.querySelector('#nav_menu_toggle_aside button'); + if (toggleBtn) { + toggleBtn.click(); + return; + } + + // Last resort: toggle .visible on .aside directly + const aside = document.querySelector('.aside'); + if (aside) { + const isVisible = aside.classList.contains('visible'); + if (isVisible) { + aside.classList.remove('visible'); + aside.style.display = 'none'; + } else { + aside.classList.add('visible'); + aside.style.display = ''; + } + } + } + + /** + * Update toggle button visual state + * @param {HTMLElement} btn + */ + function updateToggleButtonState(btn) { + if (!btn) return; + + if (gridEnabled) { + btn.classList.add('active'); + btn.style.backgroundColor = 'var(--frss-accent-color, #4a90d9)'; + btn.style.color = '#fff'; + } else { + btn.classList.remove('active'); + btn.style.backgroundColor = ''; + btn.style.color = ''; + } + } + + /** + * Toggle grid view on/off + */ + function toggleGridView() { + gridEnabled = !gridEnabled; + localStorage.setItem(STORAGE_KEY, gridEnabled); + + if (gridEnabled) { + enableGridView(); + } else { + disableGridView(); + } + + // Update toggle button state + const toggleBtn = document.querySelector('.read_grid, [data-grid-toggle]'); + updateToggleButtonState(toggleBtn); + } + + /** + * Get the current view context name for the grid header. + * Returns the category/feed name, or 'Main Stream' for all-feeds views. + * @returns {string} + */ + function getContextName() { + const params = new URLSearchParams(location.search); + const get = params.get('get') || ''; + + // All-feeds views + if (get === '' || get === 'a' || get === 'A' || get === 'Z') { + return 'Main Stream'; + } + + // Starred + if (get === 's') { + return 'Starred'; + } + + // Category view — look up name from sidebar + if (get.indexOf('c_') === 0) { + const catEl = document.querySelector('#' + get + ' > a.tree-folder-title .title'); + return catEl ? catEl.textContent.trim() : 'Main Stream'; + } + + // Feed view — show the feed name + if (get.indexOf('f_') === 0) { + const feedEl = document.querySelector('#' + get + ' > a .title'); + return feedEl ? feedEl.textContent.trim() : 'Main Stream'; + } + + return 'Main Stream'; + } + + /** + * Insert or update the grid category header above the tiles. + * @param {HTMLElement} stream + */ + function updateGridHeader(stream) { + const existing = stream.querySelector('.gridview-header'); + const name = getContextName(); + + if (existing) { + const nameEl = existing.querySelector('.gridview-header-name'); + if (nameEl) { + nameEl.textContent = name; + } else { + existing.textContent = name; + } + } else { + const header = document.createElement('div'); + header.className = 'gridview-header'; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'gridview-header-name'; + nameSpan.textContent = name; + header.appendChild(nameSpan); + + const newArticlesSpan = document.createElement('span'); + newArticlesSpan.className = 'gridview-header-new-articles'; + header.appendChild(newArticlesSpan); + + stream.insertBefore(header, stream.firstChild); + } + + // Sync notification state from native #new-article element + syncNewArticleNotification(); + } + + /** + * Remove the grid header when disabling grid view. + * @param {HTMLElement} stream + */ + function removeGridHeader(stream) { + const existing = stream.querySelector('.gridview-header'); + if (existing) { + existing.remove(); + } + } + + /** + * Wrap .nav_menu and #stream (and siblings like #slider, #close-slider) + * inside a scrollable container so that .nav_menu can use position:sticky. + * + * FreshRSS places .nav_menu, #stream, etc. as direct children of #global + * (display:table). position:sticky does not work inside table-cell layout, + * so we insert a wrapper div that becomes the scroll container. + */ + function wrapContentForStickyNav() { + // Don't wrap twice + if (document.querySelector('.gridview-scroll-wrapper')) return; + + const navMenu = document.querySelector('.nav_menu'); + if (!navMenu) return; + + const parent = navMenu.parentElement; + if (!parent) return; + + const wrapper = document.createElement('div'); + wrapper.className = 'gridview-scroll-wrapper'; + + // Collect .nav_menu and all following siblings (everything except .aside) + // These are: .nav_menu, datalist, template, #stream, #slider, #close-slider, #nav_entries + let sibling = navMenu; + const toWrap = []; + while (sibling) { + toWrap.push(sibling); + sibling = sibling.nextElementSibling; + } + + // Insert wrapper where .nav_menu was + parent.insertBefore(wrapper, toWrap[0]); + + // Move elements into wrapper + for (let i = 0; i < toWrap.length; i++) { + wrapper.appendChild(toWrap[i]); + } + } + + /** + * Unwrap the scroll container, restoring the original DOM structure. + */ + function unwrapContentForStickyNav() { + const wrapper = document.querySelector('.gridview-scroll-wrapper'); + if (!wrapper) return; + + const parent = wrapper.parentElement; + if (!parent) return; + + // Move all children back to parent, before the wrapper + while (wrapper.firstChild) { + parent.insertBefore(wrapper.firstChild, wrapper); + } + + wrapper.remove(); + } + + /** + * Set up auto-load-more for grid mode. + * + * FreshRSS's built-in onScroll() monitors document.scrollingElement, + * but in grid mode the scroll happens inside .gridview-scroll-wrapper. + * We attach our own scroll listener that clicks #load_more when the + * user scrolls near the bottom of the stream. + */ + function setupAutoLoadMore() { + // Clean up any previous handler first + teardownAutoLoadMore(); + + const wrapper = document.querySelector('.gridview-scroll-wrapper'); + if (!wrapper) return; + + let trailingTimer = null; + + /** + * Check scroll position and click load_more if near bottom. + */ + function checkAndLoad() { + const loadMoreBtn = document.getElementById('load_more'); + if (!loadMoreBtn) return; + if (loadMoreBtn.classList.contains('loading')) return; + + // Fire when within half a viewport height of the bottom of scrollable content + const distanceFromBottom = wrapper.scrollHeight - wrapper.scrollTop - wrapper.clientHeight; + if (distanceFromBottom <= wrapper.clientHeight * 0.5) { + loadMoreBtn.click(); + } + } + + autoLoadScrollHandler = function () { + // Always schedule a trailing check so the final scroll position is evaluated + if (trailingTimer) { + clearTimeout(trailingTimer); + } + trailingTimer = setTimeout(function () { + trailingTimer = null; + checkAndLoad(); + }, 200); + }; + + wrapper.addEventListener('scroll', autoLoadScrollHandler, { passive: true }); + } + + /** + * Remove the auto-load-more scroll listener. + */ + function teardownAutoLoadMore() { + if (!autoLoadScrollHandler) return; + + const wrapper = document.querySelector('.gridview-scroll-wrapper'); + if (wrapper) { + wrapper.removeEventListener('scroll', autoLoadScrollHandler); + } + autoLoadScrollHandler = null; + } + + /** @type {MutationObserver|null} */ + let newArticleObserver = null; + + /** + * Sync the native #new-article notification into the grid header bar. + * If #new-article is visible, show its text in the header; otherwise hide it. + */ + function syncNewArticleNotification() { + const newArticleEl = document.getElementById('new-article'); + const headerBadge = document.querySelector('.gridview-header-new-articles'); + if (!headerBadge) return; + + if (newArticleEl && !newArticleEl.hidden && newArticleEl.style.display !== 'none') { + const linkEl = newArticleEl.querySelector('a'); + const text = linkEl ? linkEl.textContent.trim() : newArticleEl.textContent.trim(); + headerBadge.textContent = text; + headerBadge.classList.add('visible'); + headerBadge.onclick = function () { + if (linkEl) { + linkEl.click(); + } + }; + } else { + headerBadge.classList.remove('visible'); + headerBadge.textContent = ''; + headerBadge.onclick = null; + } + } + + /** + * Observe the #new-article element for visibility changes so we can + * mirror the notification into the grid header bar. + */ + function observeNewArticleNotification() { + if (newArticleObserver) { + newArticleObserver.disconnect(); + newArticleObserver = null; + } + + const newArticleEl = document.getElementById('new-article'); + if (!newArticleEl) return; + + newArticleObserver = new MutationObserver(function () { + if (gridEnabled) { + syncNewArticleNotification(); + } + }); + + newArticleObserver.observe(newArticleEl, { + attributes: true, + attributeFilter: ['hidden', 'style'] + }); + + // Initial sync + syncNewArticleNotification(); + } + + /** + * Enable grid view + */ + function enableGridView() { + const stream = document.getElementById('stream'); + if (!stream) return; + + stream.classList.add('grid'); + stream.style.setProperty('--gridview-columns', columns); + document.body.classList.add('gridview-active'); + + // Wrap main content in a scroll container so .nav_menu can be sticky + if (stickyNavEnabled) { + wrapContentForStickyNav(); + document.body.classList.add('gridview-sticky-nav'); + } + + // Insert category header + updateGridHeader(stream); + + // Show loading skeletons while entries are being transformed. + // The CSS rule .flux:not(.gridview-transformed) { visibility:hidden } + // hides the raw list items so the default list never flashes. + showLoadingSkeleton(stream); + + // Transform existing entries + transformFluxToCards(); + + // Remove the skeleton once all current entries are transformed + removeLoadingSkeleton(stream); + + // Asynchronously fetch OG images for entries that need them + asyncFetchOgImages(); + + // Set up observer for new entries + observeNewEntries(); + + // Set up observer for state changes (read/unread/favorite) + observeFluxStateChanges(); + + // Observe #new-article notification + observeNewArticleNotification(); + + // Auto-load more articles when scrolling near the bottom + if (stickyNavEnabled) { + setupAutoLoadMore(); + } + } + + /** + * Disable grid view + */ + function disableGridView() { + const stream = document.getElementById('stream'); + if (!stream) return; + + // Disconnect observers to prevent accumulation + if (streamObserver) { + streamObserver.disconnect(); + streamObserver = null; + } + if (classObserver) { + classObserver.disconnect(); + classObserver = null; + } + teardownAutoLoadMore(); + if (newArticleObserver) { + newArticleObserver.disconnect(); + newArticleObserver = null; + } + + stream.classList.remove('grid'); + document.body.classList.remove('gridview-active'); + document.body.classList.remove('gridview-sticky-nav'); + removeGridHeader(stream); + + // Unwrap the scroll container used for sticky nav + unwrapContentForStickyNav(); + + // Restore original content + const transformedFlux = stream.querySelectorAll('.flux.gridview-transformed'); + transformedFlux.forEach(flux => { + const originalWrapper = flux.querySelector('.gridview-original-content'); + const cardWrapper = flux.querySelector('.gridview-card-content'); + + if (originalWrapper) { + // Remove card wrapper + if (cardWrapper) cardWrapper.remove(); + + // Move original content back + originalWrapper.style.display = ''; + while (originalWrapper.firstChild) { + flux.appendChild(originalWrapper.firstChild); + } + originalWrapper.remove(); + flux.classList.remove('gridview-transformed'); + // Clear any inline border style set by syncFluxVisualState + flux.style.borderLeft = ''; + } + }); + } + + /** + * Transform all .flux elements into card format + */ + function transformFluxToCards() { + const stream = document.getElementById('stream'); + if (!stream || !stream.classList.contains('grid')) return; + + const fluxElements = stream.querySelectorAll('.flux:not(.gridview-transformed)'); + + fluxElements.forEach(flux => { + transformSingleFlux(flux); + }); + } + + /** + * Transform a single flux element into a card + * @param {HTMLElement} flux + */ + function transformSingleFlux(flux) { + if (flux.classList.contains('gridview-transformed')) return; + + // Skip date separators / day dividers - don't transform them + if (flux.classList.contains('day') || + flux.classList.contains('date-separator') || + flux.classList.contains('flux_date') || + flux.tagName === 'H2' || + flux.tagName === 'H3') { + flux.classList.add('gridview-transformed'); + return; + } + + // Skip if this doesn't look like an actual article entry + // (no data-id attribute typically means it's not an article) + if (!flux.dataset.id && !flux.querySelector('.flux_content')) { + flux.classList.add('gridview-transformed'); + return; + } + + // Mark as transformed + flux.classList.add('gridview-transformed'); + + // Get the article link from data attribute first (most reliable) + let link = flux.dataset.link || ''; + + // Fallback: try to find link in the header + if (!link) { + const linkEl = flux.querySelector('.flux_header a[href^="http"], .item.title a[href^="http"], a.item.title[href^="http"]'); + if (linkEl) { + link = linkEl.href; + } + } + + // Another fallback: look for any external link in content + if (!link) { + const contentLink = flux.querySelector('.flux_content a[href^="http"]'); + if (contentLink) { + link = contentLink.href; + } + } + + // Get title - try multiple selectors + let title = ''; + const titleEl = flux.querySelector('.item.title a, .item.title .title, .title a, a.item.title'); + if (titleEl) { + title = titleEl.textContent.trim(); + } else { + // Try getting from the title span directly + const titleSpan = flux.querySelector('.item.title, .title'); + if (titleSpan) { + title = titleSpan.textContent.trim(); + } + } + + // Feed name: Normal view has .flux_header with .item.website; + // Reader view (article.phtml) has .website inside .article-header-topline or .subtitle. + // When the element is not rendered (user config), fall back to data attributes + // or the sidebar feed list using the data-feed attribute. + const feedNameEl = flux.querySelector('.flux_header .item.website a, .item.website a, .article-header-topline .website a, .subtitle .website a, .website a'); + const fluxHeader = flux.querySelector('.flux_header'); + let feedName = feedNameEl ? feedNameEl.textContent.trim() : ''; + if (!feedName && fluxHeader && fluxHeader.dataset.websiteName) { + feedName = fluxHeader.dataset.websiteName; + } + if (!feedName) { + // Last resort: look up from sidebar using data-feed attribute + const feedId = flux.dataset.feed; + if (feedId) { + const sidebarFeedEl = document.querySelector('#f_' + feedId + ' > a .title'); + if (sidebarFeedEl) { + feedName = sidebarFeedEl.textContent.trim(); + } + } + } + + // Favicon: Normal view has it in .flux_header; Reader view may have it + // in .article-header-topline. Fall back to sidebar feed favicon. + const faviconEl = flux.querySelector( + '.flux_header .favicon img, .flux_header img.favicon, .item.favicon img, ' + + '.article-header-topline .favicon img, .favicon img'); + let faviconSrc = faviconEl ? faviconEl.src : ''; + if (!faviconSrc) { + const feedId = flux.dataset.feed; + if (feedId) { + const sidebarFavicon = document.querySelector('#f_' + feedId + ' > a img'); + if (sidebarFavicon) { + faviconSrc = sidebarFavicon.src; + } + } + } + + const dateEl = flux.querySelector('.flux_header .item.date, .flux_header .date, .item.date, .subtitle .date, time'); + const date = dateEl ? dateEl.textContent.trim() : ''; + + // Author: Normal view has .item.author; Reader view renders via a helper + const authorEl = flux.querySelector('.flux_header .item.author, .item.author, .subtitle .authors, .authors'); + const author = authorEl + ? authorEl.textContent.trim() + : (fluxHeader && fluxHeader.dataset.articleAuthors ? fluxHeader.dataset.articleAuthors : ''); + + // Get thumbnail - check for existing thumbnail or extract from content + const thumbnail = getThumbnail(flux); + + // Get summary/description from content + // Prefer .text (just the description body) over .content (which may + // include FreshRSS content_header/content_footer with title/author/date) + const contentEl = flux.querySelector('.flux_content .text') || + flux.querySelector('.flux_content .content'); + const summary = contentEl ? extractSummary(contentEl.innerHTML, 150) : ''; + const fullSummary = contentEl ? extractSummary(contentEl.innerHTML, 0) : ''; + + // Get reading time if available + const readingTimeEl = flux.querySelector('.reading-time'); + const readingTime = readingTimeEl ? readingTimeEl.textContent.trim() : ''; + + // Create card HTML structure + const cardHTML = createCardHTML({ + thumbnail, + title, + link, + feedName, + faviconSrc, + summary, + fullSummary, + date, + author, + readingTime, + entryId: flux.dataset.id + }); + + // Store the article link for click handling + flux.dataset.articleLink = link; + + // Wrap original content in a hidden container (preserve for FreshRSS functionality like share) + const originalWrapper = document.createElement('div'); + originalWrapper.className = 'gridview-original-content'; + originalWrapper.style.display = 'none'; + while (flux.firstChild) { + originalWrapper.appendChild(flux.firstChild); + } + flux.appendChild(originalWrapper); + + // Add card view + const cardWrapper = document.createElement('div'); + cardWrapper.className = 'gridview-card-content'; + cardWrapper.innerHTML = cardHTML; + flux.appendChild(cardWrapper); + + // Add click handler for opening in new tab + setupCardClickHandler(flux, link, originalWrapper); + } + + /** + * Get thumbnail from flux element. + * Caches the result on the element so that view switches always + * return the same image. + * @param {HTMLElement} flux + * @returns {string|null} + */ + function getThumbnail(flux) { + // 1. Return previously cached thumbnail (survives DOM changes) + if (flux.dataset.gridviewThumb) { + return flux.dataset.gridviewThumb; + } + + let thumb = null; + + // 2. Check for a dedicated thumbnail element set by FreshRSS + const thumbnailEl = flux.querySelector('.item.thumbnail img, .thumbnail img'); + if (thumbnailEl) { + thumb = thumbnailEl.getAttribute('src') || thumbnailEl.dataset.src || null; + } + + // 2b. Check for injected thumbnail marker (for Reading View) + if (!thumb) { + const marker = flux.querySelector('.gridview-thumbnail-marker[data-thumbnail]'); + if (marker) { + thumb = marker.dataset.thumbnail; + } + } + + // 3. Check data attribute on the flux element itself + if (!thumb && flux.dataset.thumbnail) { + thumb = flux.dataset.thumbnail; + } + + // 4. Extract from content – grab raw innerHTML BEFORE any view + // switch can modify it + if (!thumb) { + const contentEl = flux.querySelector('.flux_content .text, .flux_content .content'); + if (contentEl) { + thumb = extractFirstImage(contentEl.innerHTML); + } + } + + // 5. Persist on the element so subsequent calls (after view + // switches) always return the same image + if (thumb) { + flux.dataset.gridviewThumb = thumb; + } + + return thumb; + } + + /** + * Extract first suitable image URL from raw HTML. + * + * Uses a temporary div to parse img elements but reads the original + * attribute values (getAttribute) instead of the resolved `.src` + * property, so relative URLs are preserved exactly as written by + * the feed and won't be re-resolved against the current page. + * + * @param {string} html + * @returns {string|null} + */ + function extractFirstImage(html) { + if (!html) return null; + + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = html; + + const images = tempDiv.querySelectorAll('img'); + + for (const img of images) { + // Read the *original* attribute values so the browser doesn't + // resolve them against the current page URL. + const src = img.getAttribute('src') || + img.getAttribute('data-src') || + img.getAttribute('data-lazy-src') || + img.getAttribute('data-original') || + img.getAttribute('data-lazy') || + img.getAttribute('data-srcset')?.split(',')[0]?.trim()?.split(' ')[0] || + img.getAttribute('srcset')?.split(',')[0]?.trim()?.split(' ')[0] || + null; + if (!src) continue; + + // Only consider absolute URLs (feed images should be absolute) + if (!src.startsWith('http')) continue; + + // Skip tracking pixels and known small image patterns + if (isTrackingPixel(src)) continue; + + // Skip SVG images (usually icons/logos) + if (src.toLowerCase().endsWith('.svg')) continue; + + // Skip images from theme/static asset paths (usually logos/icons) + const srcLower = src.toLowerCase(); + if (/\/(themes?|static|assets|icons?|logos?|branding|ui|images\/ic_)\//i.test(srcLower)) { + continue; + } + + // Skip images with logo/icon/badge indicators in URL, alt, or class + const alt = (img.getAttribute('alt') || '').toLowerCase(); + const className = (img.getAttribute('class') || '').toLowerCase(); + const combined = srcLower + ' ' + alt + ' ' + className; + + if (/\b(logo|icon|avatar|badge|emoji|button|brand|sprite|banner-ad)\b/.test(combined)) { + continue; + } + + // Check if image has explicit width/height attributes + const width = parseInt(img.getAttribute('width') || '0', 10); + const height = parseInt(img.getAttribute('height') || '0', 10); + + // If BOTH dimensions are explicitly specified and too small, skip. + // (Only skip when both are known — many feeds omit one or both.) + if (width > 0 && height > 0 && (width < 200 || height < 200)) { + continue; + } + + // Check for size hints in URL (e.g., ?w=800&h=533 or /800x533/) + const urlSizeMatch = src.match(/[?&]w=(\d+)/) || src.match(/\/(\d+)x\d+[/.]/); + const urlHeightMatch = src.match(/[?&]h=(\d+)/) || src.match(/\/\d+x(\d+)[/.]/); + if (urlSizeMatch && urlHeightMatch) { + const urlWidth = parseInt(urlSizeMatch[1], 10); + const urlHeight = parseInt(urlHeightMatch[1], 10); + if (urlWidth < 200 || urlHeight < 200) { + continue; + } + } + + // Skip images with small size patterns in URL + if (/(\d{1,2}x\d{1,2}|[_-](xs|s|sm|tiny|small|thumb|icon)\.|[_-]\d{2,3}w?\.)/.test(srcLower)) { + continue; + } + + return src; + } + + return null; + } + + /** + * Check if URL is likely a tracking pixel + * @param {string} url + * @returns {boolean} + */ + function isTrackingPixel(url) { + const trackingPatterns = [ + /1x1/i, + /pixel[.\-_/]/i, + /beacon/i, + /[.\-_/]track(er|ing)[.\-_/?]/i, + /analytics/i, + /\.gif$/i, + /spacer/i, + /blank\./i, + /transparent/i, + /feeds\.feedburner/i, + /feedsportal/i + ]; + + return trackingPatterns.some(pattern => pattern.test(url)); + } + + /** + * Extract plain text summary from HTML content. + * @param {string} html + * @param {number} maxLength - 0 means no truncation + * @returns {string} + */ + function extractSummary(html, maxLength = 150) { + if (!html) return ''; + + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = html; + + // Remove script and style tags + const scripts = tempDiv.querySelectorAll('script, style, noscript'); + scripts.forEach(el => el.remove()); + + // Get text content + let text = tempDiv.textContent || tempDiv.innerText || ''; + + // Clean up whitespace + text = text.replace(/\s+/g, ' ').trim(); + + // Truncate (maxLength of 0 means no truncation) + if (maxLength > 0 && text.length > maxLength) { + text = text.substring(0, maxLength).trim() + '…'; + } + + return text; + } + + /** + * Create card HTML structure + * @param {Object} data + * @returns {string} + */ + function createCardHTML(data) { + // Use placeholder image when no thumbnail is available + const placeholderFallback = placeholderUrl + ? `` + : '
📰
'; + + const onerrorFallback = placeholderUrl + ? `this.src='${escapeHtml(placeholderUrl)}'; this.classList.add('placeholder-image');` + : `this.parentElement.innerHTML='
📰
';`; + const thumbnailHTML = data.thumbnail + ? `` + : placeholderFallback; + + const faviconHTML = data.faviconSrc + ? `` + : ''; + + const readingTimeHTML = data.readingTime + ? `${escapeHtml(data.readingTime)}` + : ''; + + // Share button - triggers FreshRSS native share + const shareButtonHTML = ``; + + return ` +
+
+ ${thumbnailHTML} +
+
+ + + ${shareButtonHTML} + +
+
+
+
+ ${faviconHTML} + ${escapeHtml(data.feedName)} + ${readingTimeHTML} +
+

+ ${escapeHtml(data.title)} +

+
+

${escapeHtml(data.summary)}

+
+
${escapeHtml(data.date)}
+ ${(data.fullSummary && data.fullSummary.length > data.summary.length) + ? `

${escapeHtml(data.fullSummary)}

` + : ''} +
+ `; + } + + /** + * Escape HTML special characters + * @param {string} text + * @returns {string} + */ + function escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + /** + * Set up click handler for card + * @param {HTMLElement} flux + * @param {string} link + * @param {HTMLElement} originalWrapper - hidden wrapper containing original FreshRSS content + */ + function setupCardClickHandler(flux, link, originalWrapper) { + flux.addEventListener('click', function (e) { + // Only handle clicks when grid view is active + if (!gridEnabled) return; + + // Don't handle if clicking on action buttons + if (e.target.closest('.card-actions')) { + return; + } + + // Clicking on thumbnail toggles the action bar overlay + if (e.target.closest('.card-thumbnail')) { + e.preventDefault(); + e.stopPropagation(); + const selected = document.querySelector('.flux.card-selected'); + if (selected && selected !== flux) { + selected.classList.remove('card-selected'); + } + flux.classList.toggle('card-selected'); + return; + } + + // Clicking on title link opens the article + if (e.target.closest('.card-title')) { + e.preventDefault(); + const articleLink = flux.dataset.articleLink || flux.dataset.link || link; + if (articleLink && articleLink.startsWith('http')) { + openInReaderMode(articleLink); + markAsRead(flux); + } + return; + } + + // Clicking on summary area or tooltip toggles the expanded description + if (e.target.closest('.card-summary-wrapper') || e.target.closest('.card-summary-tooltip') || e.target.closest('.card-content')) { + const tooltip = flux.querySelector('.card-summary-tooltip'); + if (tooltip) { + tooltip.classList.toggle('expanded'); + } + } + }); + + // On mobile, tapping the card thumbnail toggles + // the action bar overlay via .card-selected class + let cardTouchMoved = false; + flux.addEventListener('touchstart', function () { + cardTouchMoved = false; + }, { passive: true }); + flux.addEventListener('touchmove', function () { + cardTouchMoved = true; + }, { passive: true }); + flux.addEventListener('touchend', function (e) { + if (cardTouchMoved || !gridEnabled) return; + // Don't interfere with title link or action button taps + if (e.target.closest('.card-title') || e.target.closest('.card-actions')) return; + + // Thumbnail tap toggles the action bar overlay + if (e.target.closest('.card-thumbnail')) { + e.preventDefault(); + e.stopPropagation(); + const selected = document.querySelector('.flux.card-selected'); + if (selected && selected !== flux) { + selected.classList.remove('card-selected'); + } + flux.classList.toggle('card-selected'); + } + }); + + // Handle action buttons + const readBtn = flux.querySelector('.card-actions .action-read'); + if (readBtn) { + readBtn.addEventListener('click', function (e) { + e.stopPropagation(); + toggleRead(flux); + }); + // Set initial state + if (flux.classList.contains('not_read')) { + readBtn.classList.add('unread'); + readBtn.title = 'Mark as read'; + } else { + readBtn.classList.remove('unread'); + readBtn.title = 'Mark as unread'; + } + } + + const starBtn = flux.querySelector('.card-actions .action-star'); + if (starBtn) { + starBtn.addEventListener('click', function (e) { + e.stopPropagation(); + e.preventDefault(); + + // Try to find the original bookmark link and use its href + if (originalWrapper) { + const originalBookmarkLink = originalWrapper.querySelector('a.bookmark, .item.manage a.bookmark, a.item-element.bookmark'); + if (originalBookmarkLink && originalBookmarkLink.href) { + // Make AJAX call like FreshRSS does + const url = originalBookmarkLink.href; + const csrfToken = window.context?.csrf || document.querySelector('input[name="_csrf"]')?.value || ''; + + fetch(url, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + ajax: true, + _csrf: csrfToken + }) + }).then(response => { + if (response.ok) { + return response.json(); + } + throw new Error('Failed to toggle favorite'); + }).then(data => { + // Toggle the favorite class + flux.classList.toggle('favorite'); + + // Update the bookmark link href with new toggle URL + if (data && data.url) { + originalBookmarkLink.href = data.url; + } + + // Update the favorites counter in sidebar + const favourites = document.querySelector('#aside_feed .favorites .title'); + if (favourites) { + const currentText = favourites.textContent; + const match = currentText.match(/\((\d+)\)/); + if (match) { + const count = parseInt(match[1], 10); + const newCount = flux.classList.contains('favorite') ? count + 1 : count - 1; + favourites.textContent = currentText.replace(/\(\d+\)/, '(' + newCount + ')'); + } + } + + // Update button state + updateStarButtonState(starBtn, flux); + }).catch(() => { + // Silently fail - the fallback will be used + }); + + return; + } + } + + // Fallback: toggle manually via AJAX using entry ID + toggleStar(flux); + updateStarButtonState(starBtn, flux); + }); + + // Set initial state + updateStarButtonState(starBtn, flux); + } + + // Intercept title link click to open in reader mode + const titleLink = flux.querySelector('.card-title-link'); + if (titleLink) { + titleLink.addEventListener('click', function (e) { + if (!gridEnabled) return; + e.preventDefault(); + e.stopPropagation(); + openInReaderMode(titleLink.href); + markAsRead(flux); + }); + + // On mobile, tapping the title should open the link directly + // without requiring the card to be selected first + let titleTouchMoved = false; + titleLink.addEventListener('touchstart', function () { + titleTouchMoved = false; + }, { passive: true }); + titleLink.addEventListener('touchmove', function () { + titleTouchMoved = true; + }, { passive: true }); + titleLink.addEventListener('touchend', function (e) { + if (titleTouchMoved || !gridEnabled) return; + e.preventDefault(); + e.stopPropagation(); + openInReaderMode(titleLink.href); + markAsRead(flux); + }); + } + + const shareBtn = flux.querySelector('.card-actions .action-share'); + if (shareBtn) { + shareBtn.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + + // Try to find the share dropdown from the original FreshRSS content + let dropdownMenu = null; + if (originalWrapper) { + const shareItem = originalWrapper.querySelector('li.item.share, .item.share'); + if (shareItem) { + dropdownMenu = shareItem.querySelector('.dropdown-menu, ul.dropdown-menu'); + if (!dropdownMenu) { + const dropdownToggle = shareItem.querySelector('.dropdown-toggle'); + if (dropdownToggle) { + dropdownToggle.click(); + setTimeout(function () { + const loadedMenu = shareItem.querySelector('.dropdown-menu, ul.dropdown-menu'); + if (loadedMenu) { + showShareDropdown(shareBtn, loadedMenu, flux); + } else { + showShareFromTemplate(shareBtn, flux); + } + }, 100); + return; + } + } + } + } + + if (dropdownMenu) { + showShareDropdown(shareBtn, dropdownMenu, flux); + } else { + showShareFromTemplate(shareBtn, flux); + } + }); + } + } + + /** + * Show share dropdown near the share button + * @param {HTMLElement} shareBtn - The share button that was clicked + * @param {HTMLElement} dropdownMenu - The original FreshRSS dropdown menu + * @param {HTMLElement} flux - The flux element + */ + function showShareDropdown(shareBtn, dropdownMenu, flux) { + // Remove any existing floating dropdowns + document.querySelectorAll('.gridview-share-dropdown').forEach(el => el.remove()); + + // Clone the dropdown menu + const dropdown = dropdownMenu.cloneNode(true); + dropdown.className = 'gridview-share-dropdown'; + + // Position it near the share button + const btnRect = shareBtn.getBoundingClientRect(); + dropdown.style.cssText = ` + position: fixed; + top: ${btnRect.bottom + 5}px; + left: ${btnRect.left}px; + z-index: 10000; + display: block; + background: var(--frss-background-color, #fff); + border: 1px solid var(--frss-border-color, #ddd); + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + padding: 0.5rem 0; + min-width: 180px; + list-style: none; + `; + + document.body.appendChild(dropdown); + + // Style the menu items + dropdown.querySelectorAll('li').forEach(li => { + li.style.listStyle = 'none'; + }); + dropdown.querySelectorAll('a').forEach(link => { + link.style.cssText = ` + display: block; + padding: 0.5rem 1rem; + color: var(--frss-text-color, #333); + text-decoration: none; + `; + link.addEventListener('mouseenter', () => { + link.style.background = 'var(--frss-background-alt, #f5f5f5)'; + }); + link.addEventListener('mouseleave', () => { + link.style.background = ''; + }); + }); + + // Close dropdown when clicking outside + const closeDropdown = (event) => { + if (!dropdown.contains(event.target) && event.target !== shareBtn) { + dropdown.remove(); + document.removeEventListener('click', closeDropdown); + document.removeEventListener('scroll', closeOnScroll, true); + } + }; + + // Close dropdown when scrolling + const closeOnScroll = () => { + dropdown.remove(); + document.removeEventListener('click', closeDropdown); + document.removeEventListener('scroll', closeOnScroll, true); + document.removeEventListener('keydown', handleEscape); + }; + + // Delay adding the listener so the current click doesn't close it + setTimeout(() => { + document.addEventListener('click', closeDropdown); + document.addEventListener('scroll', closeOnScroll, true); + }, 10); + + // Close on escape + const handleEscape = (event) => { + if (event.key === 'Escape') { + dropdown.remove(); + document.removeEventListener('keydown', handleEscape); + document.removeEventListener('click', closeDropdown); + document.removeEventListener('scroll', closeOnScroll, true); + } + }; + document.addEventListener('keydown', handleEscape); + } + + /** + * Show share dropdown built from the server-side template. + * Used when the original FreshRSS share dropdown is not available + * (e.g. Reader view where entry_share_menu is not rendered). + * @param {HTMLElement} shareBtn + * @param {HTMLElement} flux + */ + function showShareFromTemplate(shareBtn, flux) { + if (!shareMenuHtml) { + copyToClipboard(shareBtn.dataset.link, shareBtn.dataset.title); + return; + } + + const entryLink = shareBtn.dataset.link || ''; + const entryTitle = shareBtn.dataset.title || ''; + const entryId = shareBtn.dataset.entryId || ''; + + // Parse the template into DOM first, then replace placeholders + // in attribute values (not raw HTML) to avoid encoding issues + const temp = document.createElement('div'); + temp.innerHTML = shareMenuHtml; + const menu = temp.firstElementChild; + if (!menu) { + copyToClipboard(entryLink, entryTitle); + return; + } + + // Replace placeholders in link hrefs (FreshRSS does raw replacement) + menu.querySelectorAll('a[href]').forEach(function (a) { + a.href = a.getAttribute('href') + .replace(/--link--/g, entryLink) + .replace(/--titleText--/g, entryTitle) + .replace(/--entryId--/g, entryId); + }); + // Replace in form actions and hidden input values + menu.querySelectorAll('form[action]').forEach(function (form) { + form.action = form.getAttribute('action') + .replace(/--link--/g, entryLink) + .replace(/--titleText--/g, entryTitle) + .replace(/--entryId--/g, entryId); + }); + menu.querySelectorAll('input[type="hidden"]').forEach(function (input) { + input.value = (input.getAttribute('value') || '') + .replace(/--link--/g, entryLink) + .replace(/--titleText--/g, entryTitle) + .replace(/--entryId--/g, entryId); + }); + // Replace in button data attributes + menu.querySelectorAll('button[data-url]').forEach(function (btn) { + btn.dataset.url = (btn.getAttribute('data-url') || '') + .replace(/--link--/g, entryLink) + .replace(/--titleText--/g, entryTitle) + .replace(/--entryId--/g, entryId); + }); + + showShareDropdown(shareBtn, menu, flux); + } + + /** + * Copy article URL to clipboard + * @param {string} url + * @param {string} title + */ + function copyToClipboard(url, title) { + const text = title + '\n' + url; + navigator.clipboard.writeText(text).then(() => { + // Show a brief notification + showToast('Link copied to clipboard!'); + }).catch(() => { + // Fallback for older browsers + const textarea = document.createElement('textarea'); + textarea.value = text; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + showToast('Link copied to clipboard!'); + }); + } + + /** + * Open URL in a new tab. + * + * Note: Firefox blocks window.open() for about:reader URLs from web + * content, so we always open the plain URL. Users can activate their + * browser's built-in reader mode manually (e.g. F9 in Firefox). + * @param {string} url + */ + function openInReaderMode(url) { + window.open(url, '_blank', 'noopener'); + } + + /** + * Show a brief toast notification + * @param {string} message + */ + function showToast(message) { + const existing = document.querySelector('.gridview-toast'); + if (existing) existing.remove(); + + const toast = document.createElement('div'); + toast.className = 'gridview-toast'; + toast.textContent = message; + toast.style.cssText = ` + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background: #333; + color: #fff; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + z-index: 10000; + animation: gridview-toast-fade 2s ease-in-out forwards; + `; + document.body.appendChild(toast); + + setTimeout(() => toast.remove(), 2000); + } + + /** + * Mark entry as read + * @param {HTMLElement} flux + */ + function markAsRead(flux) { + if (!flux.classList.contains('not_read')) return; + + // Find the original FreshRSS read link in the hidden wrapper + // and click it so FreshRSS's own mark_read() handles persistence + const originalWrapper = flux.querySelector('.gridview-original-content'); + if (originalWrapper) { + const originalReadLink = originalWrapper.querySelector('a.read'); + if (originalReadLink) { + originalReadLink.click(); + return; + } + } + + // Fallback: use FreshRSS native function or AJAX + flux.classList.remove('not_read'); + if (typeof window.mark_read === 'function') { + window.mark_read(flux, true); + } else { + const entryId = flux.dataset.id; + if (entryId) { + const url = './?c=entry&a=read&id=' + encodeURIComponent(entryId) + '&is_read=1'; + fetch(url, { method: 'POST', credentials: 'same-origin' }).catch(function () {}); + } + } + } + + /** + * Toggle read status + * @param {HTMLElement} flux + */ + function toggleRead(flux) { + // Find the original FreshRSS read link in the hidden wrapper + // and click it so FreshRSS's own mark_read() handles persistence + const originalWrapper = flux.querySelector('.gridview-original-content'); + if (originalWrapper) { + const originalReadLink = originalWrapper.querySelector('a.read'); + if (originalReadLink) { + originalReadLink.click(); + return; + } + } + + // Fallback: toggle class and send AJAX request + const entryId = flux.dataset.id; + if (!entryId) return; + + flux.classList.toggle('not_read'); + const nowUnread = flux.classList.contains('not_read'); + const url = './?c=entry&a=read&id=' + encodeURIComponent(entryId) + '&is_read=' + (nowUnread ? '0' : '1'); + fetch(url, { method: 'POST', credentials: 'same-origin' }).catch(function () { + flux.classList.toggle('not_read'); + }); + } + + /** + * Toggle star/favorite status + * @param {HTMLElement} flux + */ + function toggleStar(flux) { + const isFavorite = flux.classList.contains('favorite'); + flux.classList.toggle('favorite'); + + const entryId = flux.dataset.id || flux.id?.replace('flux_', ''); + if (entryId) { + // Build the bookmark URL similar to FreshRSS + const url = './?c=entry&a=bookmark&id=' + encodeURIComponent(entryId) + '&is_favorite=' + (isFavorite ? '0' : '1'); + + // Get CSRF token from FreshRSS context if available + const csrfToken = window.context?.csrf || document.querySelector('input[name="_csrf"]')?.value || ''; + + fetch(url, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + ajax: true, + _csrf: csrfToken + }) + }).then(response => { + if (response.ok) { + return response.json(); + } + throw new Error('Failed to toggle favorite'); + }).then(data => { + // Update the bookmark link href if present in originalWrapper + if (data && data.url) { + const bookmarkLinks = flux.querySelectorAll('a.bookmark'); + bookmarkLinks.forEach(a => { a.href = data.url; }); + } + }).catch(() => { + // Revert on error + flux.classList.toggle('favorite'); + }); + } + } + + /** + * Update star button visual state + * @param {HTMLElement} starBtn + * @param {HTMLElement} flux + */ + function updateStarButtonState(starBtn, flux) { + if (flux.classList.contains('favorite')) { + starBtn.classList.add('active'); + starBtn.style.color = '#f5a623'; + } else { + starBtn.classList.remove('active'); + starBtn.style.color = ''; + } + } + + /** + * Asynchronously fetch OG images for entries that need them. + * Entries are identified by .gridview-og-fetch markers injected + * server-side. Cards display with placeholders immediately; when + * an OG image is retrieved, it replaces the placeholder with a + * smooth fade-in transition. + */ + function asyncFetchOgImages() { + const stream = document.getElementById('stream'); + if (!stream || !ogFetchUrl) return; + + const markers = stream.querySelectorAll('.gridview-og-fetch:not(.gridview-og-processing)'); + if (markers.length === 0) return; + + const queue = Array.from(markers); + const MAX_CONCURRENT = 3; + let active = 0; + + function processNext() { + while (queue.length > 0 && active < MAX_CONCURRENT) { + const marker = queue.shift(); + const url = marker.dataset.ogUrl; + + if (!url) continue; + + marker.classList.add('gridview-og-processing'); + + const flux = marker.closest('.flux'); + if (!flux) continue; + + active++; + + const fetchEndpoint = ogFetchUrl + '&ajax=og&url=' + encodeURIComponent(url); + + fetch(fetchEndpoint, { credentials: 'same-origin' }) + .then(function (response) { return response.json(); }) + .then(function (data) { + if (data && data.image) { + updateCardThumbnail(flux, data.image); + // Cache on the element so view-switches keep the image + flux.dataset.gridviewThumb = data.image; + } + }) + .catch(function () { /* keep placeholder on error */ }) + .finally(function () { + active--; + processNext(); + }); + } + } + + processNext(); + } + + /** + * Update a card's thumbnail with a newly fetched OG image. + * Pre-loads the image before swapping to avoid flicker. + * @param {HTMLElement} flux + * @param {string} imageUrl + */ + function updateCardThumbnail(flux, imageUrl) { + const thumbnailContainer = flux.querySelector('.card-thumbnail'); + if (!thumbnailContainer) return; + + // Pre-load the image so we only swap when it's ready + const preload = new Image(); + preload.onload = function () { + const newImg = document.createElement('img'); + newImg.src = imageUrl; + newImg.alt = ''; + newImg.loading = 'lazy'; + newImg.onerror = function () { + this.onerror = null; + if (placeholderUrl) { + this.src = placeholderUrl; + this.classList.add('placeholder-image'); + } else { + this.parentElement.innerHTML = '
📰
'; + } + }; + + // Fade transition + newImg.style.opacity = '0'; + newImg.style.transition = 'opacity 0.3s ease'; + + thumbnailContainer.innerHTML = ''; + thumbnailContainer.appendChild(newImg); + + // Trigger reflow then fade in + requestAnimationFrame(function () { + newImg.style.opacity = '1'; + }); + }; + preload.onerror = function () { /* keep current placeholder */ }; + preload.src = imageUrl; + } + + /** + * Set up mutation observer to handle dynamically loaded entries + */ + function observeNewEntries() { + const stream = document.getElementById('stream'); + if (!stream || !stream.classList.contains('grid')) return; + + // Disconnect any previous observer before creating a new one + if (streamObserver) { + streamObserver.disconnect(); + streamObserver = null; + } + + const observer = new MutationObserver(function (mutations) { + let hasNewFlux = false; + mutations.forEach(function (mutation) { + mutation.addedNodes.forEach(function (node) { + if (node.nodeType === 1 && node.classList && node.classList.contains('flux')) { + hasNewFlux = true; + transformSingleFlux(node); + } + }); + }); + // Clean up skeleton after dynamically added items are transformed + if (hasNewFlux) { + removeLoadingSkeleton(stream); + asyncFetchOgImages(); + // Re-observe all flux elements including newly added ones + observeFluxStateChanges(); + } + }); + + observer.observe(stream, { childList: true, subtree: true }); + streamObserver = observer; + } + + /** + * Set up mutation observer to watch for class changes on flux elements. + * This ensures that read/unread state changes made outside of grid view + * (or by FreshRSS core) are reflected in the grid view visual state. + */ + function observeFluxStateChanges() { + const stream = document.getElementById('stream'); + if (!stream || !stream.classList.contains('grid')) return; + + // Disconnect any previous observer before creating a new one + if (classObserver) { + classObserver.disconnect(); + classObserver = null; + } + + const observer = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + const flux = mutation.target; + // Only handle flux elements that are transformed + if (flux.classList && flux.classList.contains('flux') && + flux.classList.contains('gridview-transformed')) { + syncFluxVisualState(flux); + } + } + }); + }); + + // Observe all flux elements for class attribute changes + const fluxElements = stream.querySelectorAll('.flux.gridview-transformed'); + fluxElements.forEach(function (flux) { + observer.observe(flux, { attributes: true, attributeFilter: ['class'] }); + }); + + classObserver = observer; + + // Sync state for all elements after observer is set up + // Use requestAnimationFrame to avoid blocking the main thread + requestAnimationFrame(function () { + fluxElements.forEach(function (flux) { + syncFluxVisualState(flux); + }); + }); + } + + /** + * Synchronize the visual state of a flux element based on its classes. + * Updates read button and star button states to match the flux element's current classes. + * @param {HTMLElement} flux + */ + function syncFluxVisualState(flux) { + const isUnread = flux.classList.contains('not_read'); + + // Update read button state + const readBtn = flux.querySelector('.card-actions .action-read'); + if (readBtn) { + if (isUnread) { + readBtn.classList.add('unread'); + readBtn.title = 'Mark as read'; + } else { + readBtn.classList.remove('unread'); + readBtn.title = 'Mark as unread'; + } + } + + // Update star button state + const starBtn = flux.querySelector('.card-actions .action-star'); + if (starBtn) { + updateStarButtonState(starBtn, flux); + } + + // Explicitly set border-left as an inline style to guarantee the + // visual indicator reflects the current state. Pure CSS class + // selectors sometimes fail to re-evaluate when switching views. + if (isUnread) { + flux.style.borderLeft = '3px solid var(--frss-accent-color, #4a90d9)'; + } else { + flux.style.borderLeft = ''; + } + } + + /** + * Observe the parent of #stream so that when FreshRSS replaces + * the stream element entirely (feed navigation, AJAX reload), + * we immediately re-apply grid mode before the browser paints + * the default list layout. + */ + function observeStreamReplacement() { + if (parentObserver) { + parentObserver.disconnect(); + parentObserver = null; + } + + const stream = document.getElementById('stream'); + const parent = stream ? stream.parentElement : document.getElementById('global') || document.body; + if (!parent) return; + + parentObserver = new MutationObserver(function (mutations) { + if (!gridEnabled) return; + + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.nodeType !== 1) continue; + + // Check if the added node IS the new #stream + let newStream = null; + if (node.id === 'stream') { + newStream = node; + } else if (node.querySelector) { + newStream = node.querySelector('#stream'); + } + + if (newStream && !newStream.classList.contains('grid')) { + // Immediately apply grid mode before the browser renders + newStream.classList.add('grid'); + newStream.style.setProperty('--gridview-columns', columns); + // Re-wrap content if the scroll wrapper was lost during AJAX nav + if (stickyNavEnabled) { + wrapContentForStickyNav(); + } + updateGridHeader(newStream); + showLoadingSkeleton(newStream); + + // Transform entries in a microtask so DOM is settled + Promise.resolve().then(() => { + transformFluxToCards(); + removeLoadingSkeleton(newStream); + asyncFetchOgImages(); + observeNewEntries(); + observeFluxStateChanges(); + }); + } + } + + // Also handle the case where children of #stream are + // wholesale replaced (innerHTML swap) rather than the + // stream element itself being replaced. + if (mutation.target && mutation.target.id === 'stream') { + const target = mutation.target; + if (gridEnabled && target.classList.contains('grid')) { + // New children were added; show skeleton while transforming + const untransformed = target.querySelectorAll('.flux:not(.gridview-transformed)'); + if (untransformed.length > 0) { + showLoadingSkeleton(target); + Promise.resolve().then(() => { + transformFluxToCards(); + removeLoadingSkeleton(target); + asyncFetchOgImages(); + observeFluxStateChanges(); + }); + } + } + } + } + }); + + parentObserver.observe(parent, { childList: true, subtree: true }); + } + + /** + * Handle FreshRSS AJAX navigation: re-apply grid mode after the + * new stream content has been loaded. + */ + function handleAjaxNavigation() { + if (!gridEnabled) return; + + const stream = document.getElementById('stream'); + if (!stream) return; + + // Immediately apply grid class if missing + if (!stream.classList.contains('grid')) { + stream.classList.add('grid'); + stream.style.setProperty('--gridview-columns', columns); + } + + // Re-wrap content if the scroll wrapper was lost during AJAX nav + if (stickyNavEnabled) { + wrapContentForStickyNav(); + } + + updateGridHeader(stream); + showLoadingSkeleton(stream); + transformFluxToCards(); + removeLoadingSkeleton(stream); + asyncFetchOgImages(); + observeNewEntries(); + observeFluxStateChanges(); + observeNewArticleNotification(); + if (stickyNavEnabled) { + setupAutoLoadMore(); + } + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initGridView); + } else { + initGridView(); + } + + // Also listen for FreshRSS context loaded event + document.addEventListener('freshrss:globalContextLoaded', initGridView); + + // Re-apply grid on AJAX navigation events + if (typeof window.jQuery !== 'undefined') { + window.jQuery(document).on('freshrss:entries-loaded', handleAjaxNavigation); + } + + // FreshRSS may also fire a custom event without jQuery + document.addEventListener('freshrss:entries-loaded', handleAjaxNavigation); +})(); diff --git a/xExtension-GridView/static/placeholder.jpg b/xExtension-GridView/static/placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..003f9f6bfe46d2c75326d0b057b39fb62bf22b1f GIT binary patch literal 116480 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 Ln_-{@_&57MeUwbR literal 0 HcmV?d00001