8 Commits

Author SHA1 Message Date
mathias 0e3142bac9 remove deprecations, remove header when scrolling 2026-07-04 17:06:45 +02:00
mathias a73a1b57de padding for article view 2026-07-03 19:38:53 +02:00
mathias 3642635b20 remove variable header size 2026-07-03 11:10:14 +02:00
mathias b3cf5e4787 variable header size 2026-07-02 22:24:01 +02:00
mathias fe0adcf68e added shadow to header 2026-07-02 21:51:58 +02:00
mathias 3f9de099aa new textadjustment options 2026-06-23 18:03:11 +02:00
mathias b9c0951f2b Filter comercials in readable 2026-06-22 20:30:00 +02:00
mathias 7a24980101 Fix article get read when navigation to admin area. 2026-06-19 16:41:16 +02:00
11 changed files with 331 additions and 35 deletions
Generated
+1 -1
View File
@@ -2550,7 +2550,7 @@ dependencies = [
[[package]] [[package]]
name = "rss-reader" name = "rss-reader"
version = "0.1.0" version = "0.9.1"
dependencies = [ dependencies = [
"actix-cors", "actix-cors",
"actix-governor", "actix-governor",
+2
View File
@@ -27,6 +27,8 @@
--content-font-family: Merriweather, Georgia, 'Times New Roman', Times, serif; --content-font-family: Merriweather, Georgia, 'Times New Roman', Times, serif;
--headline-font-size-scale: 1; --headline-font-size-scale: 1;
--content-font-size-scale: 1; --content-font-size-scale: 1;
--content-text-align: left;
--content-padding: 1rem;
--color-background: var(--vt-c-white); --color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft); --color-background-soft: var(--vt-c-white-soft);
+2 -1
View File
@@ -86,7 +86,8 @@ a,
.feed-content { .feed-content {
font-family: var(--content-font-family); font-family: var(--content-font-family);
font-size: calc(clamp(1rem, 3.5vw, 1.25rem) * var(--content-font-size-scale)); font-size: calc(clamp(1rem, 3.5vw, 1.25rem) * var(--content-font-size-scale));
padding: 0 1em 1em; text-align: var(--content-text-align);
padding: 0 var(--content-padding) 1em;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
+35
View File
@@ -14,6 +14,13 @@ const {
setContentSize, setContentSize,
setHeadlineFont, setHeadlineFont,
setContentFont, setContentFont,
textAlignKey,
contentPadding,
TEXT_ALIGN_OPTIONS,
PADDING_STEPS,
PADDING_LABELS,
setTextAlign,
setContentPadding,
} = useSettings() } = useSettings()
</script> </script>
@@ -78,6 +85,34 @@ const {
>{{ opt.label }}</option> >{{ opt.label }}</option>
</select> </select>
</section> </section>
<section class="settings__section">
<h2 class="settings__section-title">Text Alignment</h2>
<div class="settings__strip">
<button
v-for="opt in TEXT_ALIGN_OPTIONS"
:key="opt.key"
class="settings__btn"
:class="{ 'settings__btn--active': textAlignKey === opt.key }"
type="button"
@click="setTextAlign(opt.key)"
>{{ opt.label }}</button>
</div>
</section>
<section class="settings__section">
<h2 class="settings__section-title">Content Padding</h2>
<div class="settings__strip">
<button
v-for="(step, i) in PADDING_STEPS"
:key="step"
class="settings__btn"
:class="{ 'settings__btn--active': contentPadding === step }"
type="button"
@click="setContentPadding(step)"
>{{ PADDING_LABELS[i] }}</button>
</div>
</section>
</div> </div>
</template> </template>
+81 -6
View File
@@ -1,18 +1,86 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { RouterLink, useRouter, useRoute } from 'vue-router' import { RouterLink, useRouter, useRoute } from 'vue-router'
import { useFeeds, logout as logoutSession } from '@/composables/useFeeds' import { useFeeds, logout as logoutSession } from '@/composables/useFeeds'
import Modal from './modal/AddUrl.vue' import Modal from './modal/AddUrl.vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const { sync, showModal, viewMode, toggleViewMode, layout, toggleLayout, markAllRead, feeds } = useFeeds() const { sync, showModal, viewMode, toggleViewMode, layout, toggleLayout, markAllRead, feeds, lastProgrammaticScroll } = useFeeds()
const headerRef = ref(null) const headerRef = ref(null)
// Scroll-driven show/hide: the header slides out of view on scroll-down and
// back in on scroll-up. This is show/hide via `transform` (not the old
// resize behaviour) — the header is position:fixed, so translating it never
// reflows content, and the app's programmatic scrolls resolve to sensible
// states: scrollTo(0, 0) lands near the top → shown; the list-view
// read-correction scrollBy moves only a few px → stays under the threshold.
const hidden = ref(false)
const REVEAL_THRESHOLD = 12 // px of accumulated travel before toggling
// When the feed list mutates itself (read-correction scrollBy + array-splice
// scroll anchoring) the page jumps *upward* without user intent. For this long
// after such a jump we gate only the reveal direction, so the jump can't pop
// the header back into view mid-read. Hiding stays allowed the whole time
// (the jump never scrolls down), so scrolling down still hides normally even
// while articles are being marked read. See lastProgrammaticScroll in useFeeds.
const PROGRAMMATIC_SUPPRESS_MS = 300
let lastY = 0
let accumulated = 0
function onScroll() {
const y = Math.max(0, window.scrollY)
const headerH = headerRef.value?.offsetHeight ?? 0
// Always reveal near the very top, and keep it visible while the menu is
// open (the dropdown is anchored to the header, so hiding it would slide the
// open menu off-screen).
if (y <= headerH || menuOpen.value) {
hidden.value = false
accumulated = 0
lastY = y
return
}
const delta = y - lastY
lastY = y
// Reset the accumulator whenever direction flips, so the threshold is
// measured from the last turning point (not from page load).
if ((delta > 0) !== (accumulated > 0)) accumulated = 0
accumulated += delta
// Hiding (scroll-down) is always allowed. Revealing (scroll-up) is gated for
// a short window after a programmatic list update, whose induced jump is
// upward and would otherwise pop the header back into view mid-read. A
// genuine scroll-up reveals once the window has elapsed.
const afterProgrammatic = performance.now() - lastProgrammaticScroll.value < PROGRAMMATIC_SUPPRESS_MS
if (accumulated > REVEAL_THRESHOLD) hidden.value = true // scrolling down
else if (accumulated < -REVEAL_THRESHOLD && !afterProgrammatic) hidden.value = false // scrolling up
}
let ticking = false
function onScrollRaf() {
if (ticking) return
ticking = true
requestAnimationFrame(() => {
onScroll()
ticking = false
})
}
onMounted(() => { onMounted(() => {
// Drives #app's padding-top / RssFeeds' scroll-margin-top so content below
// the fixed header isn't hidden behind it at scroll position 0. The header is
// a fixed size, so this is measured once on mount and never changes.
const h = headerRef.value?.getBoundingClientRect().height ?? 0 const h = headerRef.value?.getBoundingClientRect().height ?? 0
document.documentElement.style.setProperty('--app-nav-height', `${h}px`) document.documentElement.style.setProperty('--app-nav-height', `${h}px`)
lastY = Math.max(0, window.scrollY)
window.addEventListener('scroll', onScrollRaf, { passive: true })
})
onUnmounted(() => {
window.removeEventListener('scroll', onScrollRaf)
}) })
const onFeedsPage = computed(() => route.path === '/feeds') const onFeedsPage = computed(() => route.path === '/feeds')
@@ -62,7 +130,7 @@ function handleToggleLayout() {
</script> </script>
<template> <template>
<header ref="headerRef" class="app-nav"> <header ref="headerRef" class="app-nav" :class="{ 'app-nav--hidden': hidden }">
<div class="app-nav__wrapper"> <div class="app-nav__wrapper">
<span class="app-nav__title">RSS Reader<span v-if="unreadCount" class="app-nav__unread"> ({{ unreadCount }})</span></span> <span class="app-nav__title">RSS Reader<span v-if="unreadCount" class="app-nav__unread"> ({{ unreadCount }})</span></span>
<button <button
@@ -123,6 +191,13 @@ function handleToggleLayout() {
right: 0; right: 0;
z-index: 20; z-index: 20;
background: var(--color-background); background: var(--color-background);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
transition: transform 0.25s ease;
will-change: transform;
}
.app-nav--hidden {
transform: translateY(-100%);
} }
.app-nav__wrapper { .app-nav__wrapper {
@@ -131,12 +206,12 @@ function handleToggleLayout() {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.5rem; gap: 0.5rem;
padding: 0.75rem 1rem; padding: 0.375rem 1rem;
} }
.app-nav__title { .app-nav__title {
font-weight: bold; font-weight: bold;
font-size: clamp(1.1rem, 4vw, 1.4rem); font-size: clamp(0.95rem, 3.5vw, 1.1rem);
} }
.app-nav__unread { .app-nav__unread {
@@ -228,7 +303,7 @@ function handleToggleLayout() {
@media (min-width: 768px) { @media (min-width: 768px) {
.app-nav__wrapper { .app-nav__wrapper {
padding: 1rem 2rem; padding: 0.5rem 2rem;
} }
} }
</style> </style>
+10 -2
View File
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, computed, nextTick, watch } from 'vue'; import { onMounted, onBeforeUnmount, computed, nextTick, watch } from 'vue';
import { useFeeds } from '@/composables/useFeeds'; import { useFeeds } from '@/composables/useFeeds';
const { const {
@@ -14,6 +14,7 @@ const {
fetchData, fetchData,
sync, sync,
getReadable, getReadable,
disconnectObserver,
setInitialLoad, setInitialLoad,
showMessageForXSeconds, showMessageForXSeconds,
} = useFeeds() } = useFeeds()
@@ -74,6 +75,11 @@ async function shareUrl(url) {
} }
} }
onBeforeUnmount(() => {
disconnectObserver()
setInitialLoad(false)
})
onMounted(async () => { onMounted(async () => {
setInitialLoad(false) setInitialLoad(false)
await fetchData() await fetchData()
@@ -302,6 +308,7 @@ onMounted(async () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding-top: 1em;
padding-bottom: 5rem; padding-bottom: 5rem;
} }
@@ -351,7 +358,8 @@ onMounted(async () => {
} }
.article-feature__content { .article-feature__content {
padding: 0 1rem; padding: 0 var(--content-padding);
text-align: var(--content-text-align);
font-family: var(--content-font-family); font-family: var(--content-font-family);
font-size: calc(clamp(1rem, 3.5vw, 1.25rem) * var(--content-font-size-scale)); font-size: calc(clamp(1rem, 3.5vw, 1.25rem) * var(--content-font-size-scale));
line-height: 1.75; line-height: 1.75;
+131 -6
View File
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils' import { mount, flushPromises } from '@vue/test-utils'
import { nextTick } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import AppNav from '../AppNav.vue' import AppNav from '../AppNav.vue'
@@ -44,15 +45,35 @@ describe('AppNav', () => {
await router.isReady() await router.isReady()
}) })
// Unmount every AppNav mounted via mountNav() after each test so mounted
// instances (and their router/menu listeners) don't pile up across the file.
let mountedWrappers = []
function mountNav(options = { global: { plugins: [router] } }) {
const wrapper = mount(AppNav, options)
mountedWrappers.push(wrapper)
return wrapper
}
afterEach(() => {
for (const wrapper of mountedWrappers) {
try {
wrapper.unmount()
} catch {
// already unmounted by the test itself — fine
}
}
mountedWrappers = []
})
async function mountWithMenuOpen() { async function mountWithMenuOpen() {
const wrapper = mount(AppNav, { global: { plugins: [router] } }) const wrapper = mountNav()
await wrapper.find('.app-nav__hamburger').trigger('click') await wrapper.find('.app-nav__hamburger').trigger('click')
await flushPromises() await flushPromises()
return wrapper return wrapper
} }
it('toggles the menu open and closed via the hamburger button', async () => { it('toggles the menu open and closed via the hamburger button', async () => {
const wrapper = mount(AppNav, { global: { plugins: [router] } }) const wrapper = mountNav()
expect(wrapper.find('.app-nav__menu').exists()).toBe(false) expect(wrapper.find('.app-nav__menu').exists()).toBe(false)
@@ -164,7 +185,7 @@ describe('AppNav', () => {
{ id: 2, title: 'Article two', content: '', url: 'https://example.test/2', timestamp: '2026-01-02' }, { id: 2, title: 'Article two', content: '', url: 'https://example.test/2', timestamp: '2026-01-02' },
] ]
const wrapper = mount(AppNav, { global: { plugins: [router] } }) const wrapper = mountNav()
await flushPromises() await flushPromises()
expect(wrapper.find('.app-nav__title').text()).toContain('(2)') expect(wrapper.find('.app-nav__title').text()).toContain('(2)')
@@ -177,19 +198,122 @@ describe('AppNav', () => {
{ id: 2, title: 'Article two', read: false, content: '', url: 'https://example.test/2', timestamp: '2026-01-02' }, { id: 2, title: 'Article two', read: false, content: '', url: 'https://example.test/2', timestamp: '2026-01-02' },
] ]
const wrapper = mount(AppNav, { global: { plugins: [router] } }) const wrapper = mountNav()
await flushPromises() await flushPromises()
expect(wrapper.find('.app-nav__title').text()).toContain('(1)') expect(wrapper.find('.app-nav__title').text()).toContain('(1)')
}) })
it('hides the unread count when there are no articles', async () => { it('hides the unread count when there are no articles', async () => {
const wrapper = mount(AppNav, { global: { plugins: [router] } }) const wrapper = mountNav()
await flushPromises() await flushPromises()
expect(wrapper.find('.app-nav__unread').exists()).toBe(false) expect(wrapper.find('.app-nav__unread').exists()).toBe(false)
}) })
describe('scroll-driven show/hide', () => {
// The scroll handler is rAF-throttled; run rAF synchronously so a single
// dispatched scroll event resolves before we assert. Per the CLAUDE.md
// Vitest gotcha, avoid bare fake timers here — they'd clobber this stub.
beforeEach(() => {
// Reset scroll position so each mount's lastY baseline starts at 0.
Object.defineProperty(window, 'scrollY', { value: 0, configurable: true, writable: true })
vi.stubGlobal('requestAnimationFrame', (cb) => { cb(); return 0 })
// offsetHeight is 0 in jsdom; give the header a real height so the
// "near the top" guard (scrollY <= headerH) has something to compare to.
vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(50)
})
afterEach(() => {
vi.unstubAllGlobals()
vi.restoreAllMocks()
})
function scrollTo(y) {
Object.defineProperty(window, 'scrollY', { value: y, configurable: true, writable: true })
window.dispatchEvent(new Event('scroll'))
}
it('hides the header when scrolling down past the threshold', async () => {
const wrapper = mountNav()
scrollTo(200)
await nextTick()
expect(wrapper.find('header').classes()).toContain('app-nav--hidden')
})
it('reveals the header again when scrolling back up past the threshold', async () => {
const wrapper = mountNav()
scrollTo(200)
await nextTick()
expect(wrapper.find('header').classes()).toContain('app-nav--hidden')
scrollTo(150)
await nextTick()
expect(wrapper.find('header').classes()).not.toContain('app-nav--hidden')
})
it('always shows the header near the top of the page', async () => {
const wrapper = mountNav()
scrollTo(400)
await nextTick()
expect(wrapper.find('header').classes()).toContain('app-nav--hidden')
// Back within the header's own height of the top → always revealed.
scrollTo(10)
await nextTick()
expect(wrapper.find('header').classes()).not.toContain('app-nav--hidden')
})
it('does not let a programmatic upward jump reveal the header mid-read', async () => {
const { markProgrammaticScroll } = useFeeds()
const nowSpy = vi.spyOn(performance, 'now').mockReturnValue(1000)
const wrapper = mountNav()
// Hide it first via a normal scroll-down (no programmatic flag active).
scrollTo(400)
await nextTick()
expect(wrapper.find('header').classes()).toContain('app-nav--hidden')
// A read-correction flags a programmatic scroll, then the page jumps
// upward. Within the window that upward jump must NOT reveal the header.
markProgrammaticScroll() // records lastProgrammaticScroll = 1000
nowSpy.mockReturnValue(1100) // 100ms later — inside the 300ms window
scrollTo(200)
await nextTick()
expect(wrapper.find('header').classes()).toContain('app-nav--hidden')
// Still allows hiding on scroll-down even while the flag is active.
scrollTo(500)
await nextTick()
expect(wrapper.find('header').classes()).toContain('app-nav--hidden')
// Once the window elapses, a genuine scroll-up reveals it again.
nowSpy.mockReturnValue(1500) // 500ms after the flag — outside the window
scrollTo(450)
await nextTick()
expect(wrapper.find('header').classes()).not.toContain('app-nav--hidden')
})
it('does not toggle on sub-threshold jitter', async () => {
const wrapper = mountNav()
// Start well below the top so the "near the top" guard doesn't apply.
scrollTo(300)
await nextTick()
// Reveal first so we're testing that small moves don't hide it.
scrollTo(260)
await nextTick()
expect(wrapper.find('header').classes()).not.toContain('app-nav--hidden')
scrollTo(268) // +8px, under the 12px threshold
await nextTick()
expect(wrapper.find('header').classes()).not.toContain('app-nav--hidden')
})
})
it('does not mark articles as read when the confirmation is dismissed', async () => { it('does not mark articles as read when the confirmation is dismissed', async () => {
const { feeds } = useFeeds() const { feeds } = useFeeds()
feeds.value = [ feeds.value = [
@@ -208,4 +332,5 @@ describe('AppNav', () => {
confirmSpy.mockRestore() confirmSpy.mockRestore()
}) })
}) })
@@ -1,4 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest' import { describe, it, expect, vi, beforeEach } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import axios from 'axios' import axios from 'axios'
import { useFeeds } from '../useFeeds' import { useFeeds } from '../useFeeds'
+30 -2
View File
@@ -15,6 +15,16 @@ const layout = ref(localStorage.getItem('layout') || 'list') // 'list' | 'cards'
let observer; // Declare observer outside the setup function let observer; // Declare observer outside the setup function
let initialLoad = false let initialLoad = false
// Timestamp (performance.now()) of the most recent programmatic scroll / list
// mutation that moves the page without user intent — currently the list-view
// read-correction below. AppNav's auto-hide handler resyncs its scroll baseline
// (instead of treating the induced jump as a user scroll) for a short window
// after this, so removing read articles can't pop the header in/out mid-read.
const lastProgrammaticScroll = ref(0)
function markProgrammaticScroll() {
lastProgrammaticScroll.value = performance.now()
}
export function authHeaders() { export function authHeaders() {
return { return {
headers: { headers: {
@@ -134,12 +144,16 @@ async function getReadable(feed, index) {
}) })
// taz "Mehr zum Thema" related-articles teaser section. // taz "Mehr zum Thema" related-articles teaser section.
doc.querySelectorAll('#articleTeaser').forEach(el => el.remove()) doc.querySelectorAll('#articleTeaser').forEach(el => el.remove())
// taz subsidiary magazine promo blocks (e.g. taz FUTURZWEI): the promo // taz subsidiary magazine promo blocks (e.g. taz FUTURZWEI): either the
// <article> carries an aria-label containing "Abo". // <article> itself or its direct <a> child carries an aria-label containing "Abo".
doc.querySelectorAll('article[aria-label*="Abo"]').forEach(el => { doc.querySelectorAll('article[aria-label*="Abo"]').forEach(el => {
const container = el.closest('section') ?? el const container = el.closest('section') ?? el
container.remove() container.remove()
}) })
doc.querySelectorAll('article > a[aria-label*="Abo"]').forEach(el => {
const container = el.closest('section') ?? el.closest('article')
if (container) container.remove()
})
const article = new Readability(doc).parse(); const article = new Readability(doc).parse();
if (!article) { if (!article) {
showMessageForXSeconds('Could not extract readable content.', 5) showMessageForXSeconds('Could not extract readable content.', 5)
@@ -243,6 +257,9 @@ function handleIntersection(entries, topbarHeight = 0) {
observer = null observer = null
} }
// Both the array splice (via scroll anchoring) and the scrollBy correction
// below move the page — flag it so AppNav's header auto-hide ignores the jump.
markProgrammaticScroll()
const readIds = new Set(readFeeds.map(feed => feed.id)) const readIds = new Set(readFeeds.map(feed => feed.id))
feeds.value = feeds.value.filter(feed => !readIds.has(feed.id)) feeds.value = feeds.value.filter(feed => !readIds.has(feed.id))
@@ -261,6 +278,7 @@ function handleIntersection(entries, topbarHeight = 0) {
if (first) { if (first) {
const top = first.getBoundingClientRect().top const top = first.getBoundingClientRect().top
if (top < topbarHeight) { if (top < topbarHeight) {
markProgrammaticScroll()
window.scrollBy(0, top - topbarHeight) window.scrollBy(0, top - topbarHeight)
} }
} }
@@ -268,6 +286,13 @@ function handleIntersection(entries, topbarHeight = 0) {
}) })
} }
function disconnectObserver() {
if (observer) {
observer.disconnect()
observer = null
}
}
function setInitialLoad(value) { function setInitialLoad(value) {
initialLoad = value initialLoad = value
} }
@@ -375,7 +400,10 @@ export function useFeeds() {
markAllRead, markAllRead,
showMessageForXSeconds, showMessageForXSeconds,
setupIntersectionObserver, setupIntersectionObserver,
disconnectObserver,
setInitialLoad, setInitialLoad,
handleIntersection, handleIntersection,
lastProgrammaticScroll,
markProgrammaticScroll,
} }
} }
+31
View File
@@ -19,10 +19,20 @@ const CONTENT_FONT_OPTIONS = [
const SIZE_STEPS = [0.85, 1, 1.2, 1.45] const SIZE_STEPS = [0.85, 1, 1.2, 1.45]
const SIZE_LABELS = ['S', 'M', 'L', 'XL'] const SIZE_LABELS = ['S', 'M', 'L', 'XL']
const TEXT_ALIGN_OPTIONS = [
{ key: 'left', label: 'Left' },
{ key: 'justify', label: 'Justified' },
]
const PADDING_STEPS = [1, 0.5, 0.15]
const PADDING_LABELS = ['Default', 'Compact', 'Minimal']
const headlineSizeScale = ref(parseFloat(localStorage.getItem('s-headline-size') ?? '1')) const headlineSizeScale = ref(parseFloat(localStorage.getItem('s-headline-size') ?? '1'))
const contentSizeScale = ref(parseFloat(localStorage.getItem('s-content-size') ?? '1')) const contentSizeScale = ref(parseFloat(localStorage.getItem('s-content-size') ?? '1'))
const headlineFontKey = ref(localStorage.getItem('s-headline-font') ?? 'default') const headlineFontKey = ref(localStorage.getItem('s-headline-font') ?? 'default')
const contentFontKey = ref(localStorage.getItem('s-content-font') ?? 'default') const contentFontKey = ref(localStorage.getItem('s-content-font') ?? 'default')
const textAlignKey = ref(localStorage.getItem('s-text-align') ?? 'left')
const contentPadding = ref(parseFloat(localStorage.getItem('s-content-padding') ?? '1'))
function fontValue(options, key) { function fontValue(options, key) {
return (options.find(o => o.key === key) ?? options[0]).value return (options.find(o => o.key === key) ?? options[0]).value
@@ -34,6 +44,8 @@ function applySettings() {
s.setProperty('--content-font-size-scale', contentSizeScale.value) s.setProperty('--content-font-size-scale', contentSizeScale.value)
s.setProperty('--headline-font-family', fontValue(HEADLINE_FONT_OPTIONS, headlineFontKey.value)) s.setProperty('--headline-font-family', fontValue(HEADLINE_FONT_OPTIONS, headlineFontKey.value))
s.setProperty('--content-font-family', fontValue(CONTENT_FONT_OPTIONS, contentFontKey.value)) s.setProperty('--content-font-family', fontValue(CONTENT_FONT_OPTIONS, contentFontKey.value))
s.setProperty('--content-text-align', textAlignKey.value)
s.setProperty('--content-padding', contentPadding.value + 'rem')
} }
function setHeadlineSize(scale) { function setHeadlineSize(scale) {
@@ -60,6 +72,18 @@ function setContentFont(key) {
applySettings() applySettings()
} }
function setTextAlign(key) {
textAlignKey.value = key
localStorage.setItem('s-text-align', key)
applySettings()
}
function setContentPadding(step) {
contentPadding.value = step
localStorage.setItem('s-content-padding', step)
applySettings()
}
export function useSettings() { export function useSettings() {
return { return {
headlineSizeScale, headlineSizeScale,
@@ -70,10 +94,17 @@ export function useSettings() {
SIZE_LABELS, SIZE_LABELS,
HEADLINE_FONT_OPTIONS, HEADLINE_FONT_OPTIONS,
CONTENT_FONT_OPTIONS, CONTENT_FONT_OPTIONS,
TEXT_ALIGN_OPTIONS,
PADDING_STEPS,
PADDING_LABELS,
applySettings, applySettings,
setHeadlineSize, setHeadlineSize,
setContentSize, setContentSize,
setHeadlineFont, setHeadlineFont,
setContentFont, setContentFont,
setTextAlign,
setContentPadding,
textAlignKey,
contentPadding,
} }
} }
+7 -17
View File
@@ -31,23 +31,13 @@ const router = createRouter({
] ]
}) })
router.beforeEach((to, from, next) => { router.beforeEach((to) => {
if (to.meta.requiresAuth) { const isAuthenticated = localStorage.getItem("user-token") != null;
let isAuthenticated = false; // Redirect unauthenticated users hitting a protected route to login;
if (localStorage.getItem("user-token") != null){ // returning a value (instead of the deprecated next() callback) is the
isAuthenticated = true; // modern vue-router guard API. Returning nothing lets navigation proceed.
} if (to.meta.requiresAuth && !isAuthenticated) {
return '/login';
if (!isAuthenticated) {
// Redirect to the login page
next('/login');
} else {
// Proceed to the protected route
next();
}
} else {
// For routes that don't require authentication, proceed without checking
next();
} }
}); });
export default router export default router