card view, minor css bugfixes

This commit is contained in:
2026-06-08 14:22:04 +02:00
parent 3c42ebb972
commit a98a8ba9e6
6 changed files with 275 additions and 17 deletions
+49 -11
View File
@@ -10,6 +10,7 @@ const message = ref('')
const showModal = ref(false)
const viewMode = ref('list') // 'list' | 'article' — toggled from the hamburger menu
const currentIndex = ref(0)
const layout = ref('list') // 'list' | 'cards' — list-view display style, toggled from the hamburger menu
let observer; // Declare observer outside the setup function
let initialLoad = false
@@ -23,18 +24,33 @@ function authHeaders() {
}
}
// Some feeds (e.g. Deutsche Welle) ship <img> tags whose `src`/`data-url`
// contain an unresolved `${formatId}` template that their own frontend fills
// in from the sibling `data-format` attribute before loading — verbatim they
// 404. Resolve them the same way here, or drop the <img> if we can't, so
// Readability doesn't carry a broken image into the parsed article.
// Some feeds (e.g. Deutsche Welle) ship <img> tags whose `src` and various
// lazy-load attributes (`data-url`, `data-src`, `srcset`, ...) contain an
// unresolved `${placeholderName}` template — or its URL-encoded `%7B...%7D`
// form — that their own frontend fills in from the sibling `data-format`
// attribute before loading; verbatim they 404. Resolve every such attribute
// the same way (so Readability's own lazy-image handling can't resurrect a
// stale template into `src`), preferring `data-url` as the source of truth
// for `src`, and drop the <img> entirely if a template still remains.
const TEMPLATE_PATTERN = /\$\{[^}]+\}|%7[bB][^%]*%7[dD]/
const TEMPLATE_PATTERN_GLOBAL = /\$\{[^}]+\}|%7[bB][^%]*%7[dD]/g
function resolveTemplatedImage(img) {
const placeholder = '${formatId}'
const format = img.getAttribute('data-format')
const dataUrl = img.getAttribute('data-url')
if (format && dataUrl && dataUrl.includes(placeholder)) {
img.setAttribute('src', dataUrl.replace(placeholder, format))
} else if (/[{]|%7[bB]/.test(img.getAttribute('src') ?? '')) {
if (format) {
if (dataUrl && TEMPLATE_PATTERN.test(dataUrl)) {
img.setAttribute('src', dataUrl.replace(TEMPLATE_PATTERN_GLOBAL, format))
}
for (const attr of [...img.attributes]) {
if (attr.name !== 'src' && TEMPLATE_PATTERN.test(attr.value)) {
img.setAttribute(attr.name, attr.value.replace(TEMPLATE_PATTERN_GLOBAL, format))
}
}
}
if (TEMPLATE_PATTERN.test(img.getAttribute('src') ?? '')) {
img.remove()
}
}
@@ -175,17 +191,36 @@ function markCurrentArticleRead() {
const feed = feeds.value[currentIndex.value]
// Marking read here (rather than via removeFeed, as the scroll-based list
// view does) keeps the array stable so currentIndex stays valid while paging.
if (feed) markRead(feed.id)
// The local `read` flag lets leaveArticleView() drop these once we're done.
if (feed) {
feed.read = true
markRead(feed.id)
}
}
function leaveArticleView() {
// Articles paged past in article view were marked read but deliberately kept
// in place so currentIndex stayed valid — drop them now so they don't keep
// showing up in the list view.
feeds.value = feeds.value.filter(feed => !feed.read)
currentIndex.value = 0
viewMode.value = 'list'
}
function toggleViewMode() {
viewMode.value = viewMode.value === 'list' ? 'article' : 'list'
if (viewMode.value === 'article') {
leaveArticleView()
} else {
viewMode.value = 'article'
currentIndex.value = 0
markCurrentArticleRead()
}
}
function toggleLayout() {
layout.value = layout.value === 'list' ? 'cards' : 'list'
}
function nextArticle() {
if (currentIndex.value < feeds.value.length - 1) {
currentIndex.value += 1
@@ -209,6 +244,9 @@ export function useFeeds() {
viewMode,
currentIndex,
toggleViewMode,
leaveArticleView,
layout,
toggleLayout,
nextArticle,
prevArticle,
fetchData,