Some checks failed
Lint & Test / test (push) Has been cancelled
- Dismissible donation/open-source banner after 3+ sessions (30-day snooze) - New About tab in Settings: version, repo link, license info - Centralize project URLs (REPO_URL, DONATE_URL) in __init__.py, served via /health - Center settings tab bar, reduce tab padding for 6-tab fit - External URL save button: icon button instead of full-width text button - Remove redundant settings footer close button - Footer "Source Code" link replaced with "About" opening settings - i18n keys for en/ru/zh
144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
/**
|
|
* Donation banner — shows a dismissible open-source/donation notice
|
|
* after the user has had a few sessions with the app.
|
|
*/
|
|
|
|
import { t } from '../core/i18n.ts';
|
|
import { ICON_HEART, ICON_EXTERNAL_LINK, ICON_X, ICON_GITHUB } from '../core/icons.ts';
|
|
|
|
// ─── Config ─────────────────────────────────────────────────
|
|
|
|
/** URLs are set from the server /health response via setProjectUrls(). */
|
|
let _donateUrl = '';
|
|
let _repoUrl = '';
|
|
|
|
/** Minimum number of app opens before showing the banner. */
|
|
const MIN_SESSIONS = 3;
|
|
|
|
/** "Remind me later" snooze duration (30 days). */
|
|
const SNOOZE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
|
|
// ─── localStorage keys ──────────────────────────────────────
|
|
|
|
const LS_DISMISSED = 'donation-banner-dismissed';
|
|
const LS_SNOOZE = 'donation-banner-snooze-until';
|
|
const LS_SESSIONS = 'donation-banner-sessions';
|
|
|
|
// ─── Public API ─────────────────────────────────────────────
|
|
|
|
/** Set project URLs from server health response. Call before initDonationBanner. */
|
|
export function setProjectUrls(repoUrl: string, donateUrl: string): void {
|
|
_repoUrl = repoUrl || '';
|
|
_donateUrl = donateUrl || '';
|
|
}
|
|
|
|
/** Call once on app init. Increments session count and shows banner if conditions met. */
|
|
export function initDonationBanner(): void {
|
|
// No URLs configured — nothing to show
|
|
if (!_donateUrl && !_repoUrl) return;
|
|
|
|
const sessions = (parseInt(localStorage.getItem(LS_SESSIONS) || '0', 10) || 0) + 1;
|
|
localStorage.setItem(LS_SESSIONS, String(sessions));
|
|
|
|
if (sessions < MIN_SESSIONS) return;
|
|
if (localStorage.getItem(LS_DISMISSED) === '1') return;
|
|
|
|
const snoozeUntil = parseInt(localStorage.getItem(LS_SNOOZE) || '0', 10) || 0;
|
|
if (snoozeUntil > Date.now()) return;
|
|
|
|
_showBanner();
|
|
}
|
|
|
|
/** Dismiss forever. */
|
|
export function dismissDonation(): void {
|
|
localStorage.setItem(LS_DISMISSED, '1');
|
|
_hideBanner();
|
|
}
|
|
|
|
/** Snooze for 30 days. */
|
|
export function snoozeDonation(): void {
|
|
localStorage.setItem(LS_SNOOZE, String(Date.now() + SNOOZE_MS));
|
|
_hideBanner();
|
|
}
|
|
|
|
/** Render the About panel content in settings modal. */
|
|
export function renderAboutPanel(): void {
|
|
const container = document.getElementById('about-panel-content');
|
|
if (!container) return;
|
|
|
|
const version = document.getElementById('version-number')?.textContent || '';
|
|
|
|
let links = '';
|
|
|
|
if (_repoUrl) {
|
|
links += `<a href="${_repoUrl}" target="_blank" rel="noopener" class="about-link">
|
|
${ICON_GITHUB}
|
|
<span>${t('donation.view_source')}</span>
|
|
${ICON_EXTERNAL_LINK}
|
|
</a>`;
|
|
}
|
|
|
|
if (_donateUrl) {
|
|
links += `<a href="${_donateUrl}" target="_blank" rel="noopener" class="about-link about-link-donate">
|
|
${ICON_HEART}
|
|
<span>${t('donation.about_donate')}</span>
|
|
${ICON_EXTERNAL_LINK}
|
|
</a>`;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<div class="about-section">
|
|
<div class="about-logo">${ICON_HEART}</div>
|
|
<h3 class="about-title">${t('donation.about_title')}</h3>
|
|
${version ? `<span class="about-version">${version}</span>` : ''}
|
|
<p class="about-text">${t('donation.about_opensource')}</p>
|
|
${links ? `<div class="about-links">${links}</div>` : ''}
|
|
<p class="about-license">${t('donation.about_license')}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ─── Internal ───────────────────────────────────────────────
|
|
|
|
function _showBanner(): void {
|
|
const banner = document.getElementById('donation-banner');
|
|
if (!banner) return;
|
|
|
|
let actions = '';
|
|
|
|
if (_donateUrl) {
|
|
actions += `<a href="${_donateUrl}" target="_blank" rel="noopener"
|
|
class="btn btn-icon donation-banner-action donation-banner-donate"
|
|
title="${t('donation.support')}">
|
|
${ICON_HEART}
|
|
</a>`;
|
|
}
|
|
|
|
if (_repoUrl) {
|
|
actions += `<a href="${_repoUrl}" target="_blank" rel="noopener"
|
|
class="btn btn-icon donation-banner-action"
|
|
title="${t('donation.view_source')}">
|
|
${ICON_GITHUB}
|
|
</a>`;
|
|
}
|
|
|
|
actions += `<button class="btn btn-icon donation-banner-action"
|
|
onclick="snoozeDonation()" title="${t('donation.later')}">
|
|
${ICON_X}
|
|
</button>`;
|
|
|
|
banner.innerHTML = `
|
|
<span class="donation-banner-text">
|
|
${ICON_HEART}
|
|
${t('donation.message')}
|
|
</span>
|
|
${actions}
|
|
`;
|
|
banner.style.display = 'flex';
|
|
}
|
|
|
|
function _hideBanner(): void {
|
|
const banner = document.getElementById('donation-banner');
|
|
if (banner) banner.style.display = 'none';
|
|
}
|