Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions core/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,6 @@ export function renderIndexPage(data: PageData): string {
<body data-app-version="${escapeHtml(data.appVersion)}">
<header class="shell page-header">
<div class="toast" data-toast role="status" aria-live="polite"></div>
<div class="update-banner is-hidden" data-update-banner role="status" aria-live="polite">
<span>A new version is available.</span>
<button class="update-banner-button" type="button" data-update-refresh>Refresh</button>
</div>
<div class="header-top">
<h1 class="brand"><img class="brand-mark" src="/favicon.svg?v=7" alt="" aria-hidden="true" /><span>reader</span></h1>
<div class="header-actions">
Expand All @@ -173,12 +169,22 @@ export function renderIndexPage(data: PageData): string {
<path d="M4 4l16 16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.9"/>
</svg>
</span>
<button class="icon-button install-button is-hidden" type="button" data-install-button aria-label="Install reader" title="Install reader">
<button class="pill-button update-button is-hidden" type="button" data-update-button aria-label="A new version is available — tap to refresh" title="A new version is available — tap to refresh">
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2.5c2.6 2 4 5.3 4 9.3 0 2.4-.5 4.4-1.3 6l-2.7 3-2.7-3c-.8-1.6-1.3-3.6-1.3-6 0-4 1.4-7.3 4-9.3Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/>
<circle cx="12" cy="10.5" r="1.5" fill="none" stroke="currentColor" stroke-width="1.8"/>
<path d="M8.7 14.5l-2.6 1.3.4-3.2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/>
<path d="M15.3 14.5l2.6 1.3-.4-3.2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/>
</svg>
<span>New version</span>
</button>
<button class="pill-button install-button is-hidden" type="button" data-install-button aria-label="Install reader" title="Install reader">
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 3.5v10.6" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.8"/>
<path d="M7.8 10.7l4.2 4.2 4.2-4.2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/>
<path d="M4.5 18.2h15" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.8"/>
</svg>
<span>Install</span>
</button>
<button class="icon-button refresh-button" type="button" data-refresh-button aria-label="Refresh feed" title="Refresh feed">
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true">
Expand Down Expand Up @@ -379,6 +385,7 @@ export function renderIndexPage(data: PageData): string {
</ol>
<div class="config-dialog-footer-row" data-install-dialog-footer>
<div class="config-dialog-actions">
<button class="dialog-button" type="button" data-install-dialog-hide>Don't show again</button>
<button class="dialog-button dialog-button-primary" type="button" data-install-dialog-confirm>
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 3.5v10.6" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.8"/>
Expand Down
43 changes: 34 additions & 9 deletions web-static/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
const installConfirmButton = document.querySelector(
"[data-install-dialog-confirm]",
);
const installDialogFooter = document.querySelector(
"[data-install-dialog-footer]",
const installDialogHideButton = document.querySelector(
"[data-install-dialog-hide]",
);
const installSteps = document.querySelector("[data-install-steps]");
const installScreenshotMobile = document.querySelector(
Expand All @@ -67,15 +67,15 @@
"[data-install-screenshot-desktop]",
);
const toast = document.querySelector("[data-toast]");
const updateBanner = document.querySelector("[data-update-banner]");
const updateRefreshButton = document.querySelector("[data-update-refresh]");
const updateButton = document.querySelector("[data-update-button]");
const pageSize = Number(cardsGrid?.dataset.pageSize || 12);
const searchDebounceMs = 1100;
const sourceConfigStorageKey = "feedreader.sources";
const densityConfigStorageKey = "feedreader.uiDensity";
const visitedLinksStorageKey = "feedreader.visited";
const visitedLinksLimit = 500;
const themeStorageKey = "feedreader.theme";
const installPromptHiddenStorageKey = "feedreader.installPromptHidden";
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
const loadedAppVersion = document.body.dataset.appVersion || "";
const updateCheckIntervalMs = 15 * 60 * 1000;
Expand All @@ -84,6 +84,7 @@
let selectedSources = loadSelectedSources();
let uiDensity = loadUIDensity();
let visitedLinks = loadVisitedLinks();
let installPromptHidden = loadInstallPromptHidden();
let activeQuery = (searchInput?.value || "").trim();
let searchOpen = Boolean(activeQuery);
let loadedCount = cardsGrid
Expand Down Expand Up @@ -299,6 +300,18 @@
localStorage.setItem(densityConfigStorageKey, uiDensity);
}

function loadInstallPromptHidden() {
try {
return localStorage.getItem(installPromptHiddenStorageKey) === "true";
} catch {
return false;
}
}

function persistInstallPromptHidden() {
localStorage.setItem(installPromptHiddenStorageKey, "true");
}

function syncThemeOptions() {
themeOptions.forEach((option) => {
option.checked = option.value === root.dataset.theme;
Expand Down Expand Up @@ -394,8 +407,8 @@
renderConnectionIndicator();
};

const showUpdateBanner = () => {
updateBanner?.classList.remove("is-hidden");
const showUpdateButton = () => {
updateButton?.classList.remove("is-hidden");
};

let checkingForUpdate = false;
Expand All @@ -407,7 +420,7 @@
if (!response.ok) return;
const payload = await response.json();
if (payload.version && payload.version !== loadedAppVersion) {
showUpdateBanner();
showUpdateButton();
}
} catch {
// Offline or request failed — try again on the next visibility/focus
Expand Down Expand Up @@ -867,6 +880,7 @@
}

function showInstallButton() {
if (installPromptHidden) return;
installButton?.classList.remove("is-hidden");
}

Expand Down Expand Up @@ -1030,6 +1044,15 @@
});
}

if (installDialogHideButton) {
installDialogHideButton.addEventListener("click", () => {
installPromptHidden = true;
persistInstallPromptHidden();
hideInstallButton();
closeInstallDialog();
});
}

if (installDialog) {
installDialog.addEventListener("cancel", (event) => {
event.preventDefault();
Expand Down Expand Up @@ -1173,7 +1196,9 @@
if (!isStandaloneDisplay()) {
if (isIOSDevice()) {
installSteps?.classList.remove("is-hidden");
installDialogFooter?.classList.add("is-hidden");
// No native install prompt on iOS, so there's nothing for the primary
// button to confirm — keep "Don't show again" as the only footer action.
installConfirmButton?.classList.add("is-hidden");
showInstallButton();
}

Expand Down Expand Up @@ -1211,7 +1236,7 @@
});
}

updateRefreshButton?.addEventListener("click", () => {
updateButton?.addEventListener("click", () => {
window.location.reload();
});

Expand Down
62 changes: 32 additions & 30 deletions web-static/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ button {

.header-actions {
display: inline-flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
gap: 0.45rem;
}
Expand Down Expand Up @@ -220,6 +222,31 @@ button {
cursor: wait;
}

.pill-button {
display: inline-flex;
align-items: center;
gap: 0.35rem;
height: var(--control-height);
padding: 0 0.85rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--accent) 44%, var(--border));
background: var(--accent-soft);
color: var(--text);
font-size: 0.86rem;
font-weight: 500;
cursor: pointer;
box-shadow: var(--shadow);
transition: 140ms ease;
}

.pill-button:hover {
border-color: color-mix(in srgb, var(--accent) 60%, var(--border));
}

.pill-button .theme-icon {
color: var(--accent);
}

.search-toggle.is-active,
.search-clear {
color: var(--text);
Expand Down Expand Up @@ -588,6 +615,11 @@ body.is-dialog-open {
.config-dialog-actions {
display: flex;
justify-content: flex-end;
/* Pushes this flex item fully right within .config-dialog-footer-row even
* when it's the row's only child — without this, justify-content:
* space-between on the row has nothing to space against and the actions
* end up sitting at the start (left) instead, as in the install dialog. */
margin-left: auto;
gap: 0.55rem;
}

Expand Down Expand Up @@ -701,36 +733,6 @@ body.is-dialog-open {
display: none;
}

.update-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
padding: 0.55rem 0.7rem 0.55rem 0.9rem;
border-radius: 0.9rem;
border: 1px solid color-mix(in srgb, var(--accent) 36%, var(--border));
background: var(--accent-soft);
color: var(--text);
font-size: 0.88rem;
}

.update-banner-button {
flex: 0 0 auto;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
cursor: pointer;
border-radius: 999px;
padding: 0.4rem 0.9rem;
font-size: 0.85rem;
font-weight: 600;
transition: 140ms ease;
}

.update-banner-button:hover {
border-color: color-mix(in srgb, var(--accent) 44%, var(--border));
}

.page-body {
padding-bottom: calc(0.35rem + env(safe-area-inset-bottom));
}
Expand Down