mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* fix(settings): hide cached title matches the parental lock withholds Cross-playlist title matches are cached by their consumers (Actor and Discover routes, the dashboard trending and recommendation rails and the four "similar in your portals" rails), so matches found while unlocked kept advertising locked titles and their playlist names after a relock. Each consumer now filters on read through a reactive predicate on the match's provider category, so a relock hides them at once and an unlock shows them again, without a re-query. Closes #1723 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fall back to unlocked copies of a withheld title match The dashboard trending and recommendation rails and the "similar in your portals" rails picked one match per title at load, so hiding a withheld match lost a copy of the same title in an unlocked portal. They now keep every row the lookup returned and pick the match on read from the rows the parental lock does not withhold. Adds the release note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): derive the recommendations seed heading from the visible cards The rail kept only the seeds that contributed a card to the original selection, so a seed whose recommendations filled in after a relock (or an offline prune) was missing from the "Because you watched" heading. Every seed of the load is kept in order and the heading lists those that contribute a card now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
58 lines
1.8 KiB
TypeScript
58 lines
1.8 KiB
TypeScript
/** Warn this many days before a portal subscription lapses. */
|
|
export const SOURCE_EXPIRY_WARNING_DAYS = 7;
|
|
|
|
/**
|
|
* How often badge consumers should re-evaluate {@link resolveSourceExpiryBadge}
|
|
* against the wall clock. The badge state only moves at day granularity, but a
|
|
* dashboard left open must still cross day-countdown and expiration
|
|
* boundaries without a remount; a minute tick is imperceptibly cheap.
|
|
*/
|
|
export const SOURCE_EXPIRY_TICK_MS = 60_000;
|
|
|
|
const SECONDS_PER_DAY = 86_400;
|
|
|
|
/**
|
|
* What a source's account facts say about its subscription lifetime.
|
|
* `reportedExpired` covers portals that answer with an expired account
|
|
* status without a usable timestamp; `expiresAtSeconds` is unix seconds
|
|
* when the portal reports one.
|
|
*/
|
|
export interface SourceExpiryFacts {
|
|
expiresAtSeconds: number | null;
|
|
reportedExpired: boolean;
|
|
}
|
|
|
|
export type SourceExpiryBadge =
|
|
{ kind: 'expired' } | { kind: 'expiring'; daysLeft: number };
|
|
|
|
/**
|
|
* Decides whether a source card should carry an expiry badge. Returns null
|
|
* for unknown expirations and for subscriptions further out than
|
|
* `warningDays` — the badge is a warning, not a countdown.
|
|
*/
|
|
export function resolveSourceExpiryBadge(
|
|
facts: SourceExpiryFacts | null | undefined,
|
|
nowMs: number,
|
|
warningDays: number = SOURCE_EXPIRY_WARNING_DAYS
|
|
): SourceExpiryBadge | null {
|
|
if (!facts) {
|
|
return null;
|
|
}
|
|
if (facts.reportedExpired) {
|
|
return { kind: 'expired' };
|
|
}
|
|
|
|
const expiresAt = facts.expiresAtSeconds;
|
|
if (expiresAt === null || expiresAt <= 0) {
|
|
return null;
|
|
}
|
|
|
|
const secondsLeft = expiresAt - nowMs / 1000;
|
|
if (secondsLeft <= 0) {
|
|
return { kind: 'expired' };
|
|
}
|
|
|
|
const daysLeft = Math.ceil(secondsLeft / SECONDS_PER_DAY);
|
|
return daysLeft <= warningDays ? { kind: 'expiring', daysLeft } : null;
|
|
}
|