aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* style: apply biome formatterHEADmainFuwn8 days4-9/+14
|
* refactor(airing): unify countdown formatter into shared helperFuwn8 days3-121/+67
|
* fix(airing): round residual hour and minute in AiringTime componentFuwn8 days1-8/+8
|
* fix(airing): floor hour consistently in hours countdownFuwn8 days1-3/+9
|
* fix(airing): round residual hour in days countdownFuwn8 days1-2/+7
|
* feat(command-palette): add quick toggles, sync, and auth actionsFuwn10 days4-1/+326
| | | | | | | | Adds 13 reactive quick toggles (24h time, animations, blur adult, cover modes, hover cover, schedule list, reverse sort, data saver, notifications, language, title format cycle, outbound link target cycle), three sync actions (push/pull/disable), and login/logout entries gated on auth state. Names reflect current state so the palette doubles as a status surface.
* fix(tooltip): park off-screen on create, enable glide after first placementFuwn2026-04-192-4/+9
| | | | | | | | | | | | | | | The tooltip was appended to body with no top/left, so its static-flow position extended body.scrollWidth/scrollHeight during measurement. Fix by setting initial top/left to -9999px so the node never lands in the flow. Previously this slid the tooltip in from off-screen because the top/left 0.3s transition was already live. Now the transition starts as opacity-only at creation time; after the first updateTooltipPosition snaps into place, a requestAnimationFrame re-enables the top/left easing so subsequent mousemove updates keep the original smooth glide and 100ms debounce behavior. LinkedTooltip's measurement div is parked the same way plus visibility:hidden, since it exists only to read offsetWidth for the Svelte-rendered tooltip.
* Revert "fix(tooltip): use fixed positioning and snappy cursor tracking"Fuwn2026-04-192-12/+15
| | | | This reverts commit 28860bb88da4c08e3ba383adc9c23ae3689310b6.
* fix(tooltip): use fixed positioning and snappy cursor trackingFuwn2026-04-192-15/+12
| | | | | | | | | | | | | Body grew on hover because the absolutely-positioned tooltip was appended to document.body without top/left, so its static-flow position extended body.scrollWidth/scrollHeight during layout measurement. Switch both the use:tooltip directive and LinkedTooltip's measurement div to position:fixed, which is relative to the viewport and does not contribute to the body's scroll area. The directive now also uses clientX/clientY (dropping the scrollY offset for the pin branch), removes the 100ms mousemove debounce, and drops the top/left transition so the tooltip tracks the cursor without feeling laggy or sliding in from the page top.
* Revert "fix(tooltip): park measurement node off-screen to stop body resize"Fuwn2026-04-192-5/+0
| | | | This reverts commit 6865cae76cb88aa78d7c297c637557468fdce8fc.
* Revert "fix(tooltip): drop position transition so tooltip snaps instead of ↵Fuwn2026-04-191-1/+1
| | | | | | sliding" This reverts commit 6e780fdb76155669a72692c64fd51c0da2c932d2.
* fix(tooltip): drop position transition so tooltip snaps instead of slidingFuwn2026-04-191-1/+1
| | | | | | | The opacity+top+left transition combined with the off-screen parking caused tooltips to animate from -9999px into place ("pops down from the top") and lagged cursor tracking in the non-pinned variant used by birthdays. Keep only the opacity fade; position updates snap.
* fix(tooltip): park measurement node off-screen to stop body resizeFuwn2026-04-192-0/+5
| | | | | | Absolute-positioned tooltips were appended to document.body without initial left/top, so their static-flow position extended body scrollWidth/scrollHeight during layout, briefly enlarging the page on hover.
* refactor(wrapped): simplify redundant new Date(Date.now()) callsFuwn2026-04-181-2/+2
| | | | | | new Date(Date.now()) is identical to new Date() and was called twice back-to-back to seed currentYear and selectedYear to the same value. Call new Date() once for currentYear and reuse it for selectedYear.
* refactor(naming): replace banned ctx abbreviation with full namesFuwn2026-04-182-9/+9
| | | | | | | | | | | | | CLAUDE.md prohibits abbreviations like ctx in identifiers. Rename the Cloudflare Worker ExecutionContext parameter to executionContext in the proxy worker (handleMangaChapterCounts, fetch, scheduled), and alias Trigger.dev's destructured { ctx } to taskContext in the notifications scheduled task. The external property name on Trigger.dev's params object is library-defined and remains ctx on the wire. Verified: proxy worker still boots under local wrangler dev and all routes (OPTIONS, POST /manga/chapter-counts, forwardProxyRequest) still respond identically.
* Revert "fix(api): drop unused redirect query param from oauth refresh"Fuwn2026-04-181-1/+3
| | | | This reverts commit 13226aaeb7c4dc1ce01074ef1ba1eeb87b53d5f5.
* fix(settings): propagate inner json/handler errors to outer catchFuwn2026-04-181-35/+36
| | | | | | | | | | | | | | | | | Earlier fix added .catch() handlers to both fetch chains, but the outer .then body called response.json().then(...) without returning the inner promise. That meant rejections from response.json() (e.g. non-JSON response) or from the inner handler body (e.g. a throw in isEqualsJson or data access) floated unhandled. Return the inner promise so its rejection reaches the outer .catch. Also short-circuit early on !response.ok to keep the happy path at the top indentation level. Verified via a standalone promise-graph reproducer: all seven paths (outer fetch reject, non-ok response, json reject, handler throw, push fetch reject, push handler throw, happy path) are caught by their intended handler.
* build(workers): bump wrangler compatibility_date to 2025-01-01Fuwn2026-04-182-2/+2
| | | | | | | | | | Both the CDN (2023-10-30) and proxy (2023-12-18) workers were pinned to 2023 compatibility dates, missing ~2 years of Cloudflare Workers runtime fixes and API behaviour corrections. Bump both to 2025-01-01. Verified: both workers boot cleanly under local wrangler dev on the new date, and the CDN worker still returns correct cache/CORS/content -type headers on a sample PNG request.
* fix(tooltip): remove body-appended tooltip node on destroyFuwn2026-04-181-0/+12
| | | | | | | | | | | | LinkedTooltip appends its tooltip div directly to document.body in createTooltip and only removes it from hideTooltip. If the component was destroyed while the tooltip was visible (e.g. parent navigated mid-hover), the div leaked and stayed in the DOM forever. Add an onDestroy handler that clears the pending hide/debounce timers and, if the tooltip div is still parented to document.body, removes it. Guarding on parentNode avoids double-removal when hideTooltip has already run.
* fix(notification): clear auto-dismiss timer on component destroyFuwn2026-04-181-1/+5
| | | | | | | | | onMount scheduled a setTimeout to auto-dismiss the notification after notification.duration ms but never stored the handle. If the component was destroyed before the timer fired (parent unmount, navigation), the timer still ran and called remove() on a stale closure. Capture the timer id and clear it from the onMount cleanup callback.
* fix(settings): catch errors in settings sync fetch chainsFuwn2026-04-181-12/+16
| | | | | | | | | | | The settings store's subscribe callback kicked off a fetch chain to pull/push settings with no .catch() on either promise chain. A network failure or non-ok PUT from /api/configuration would surface as an unhandled promise rejection. Attach a .catch() that logs a descriptive error on both the pull and push chains so sync failures become observable without crashing the page.
* fix(api): await setShadowHidden in badges PUTFuwn2026-04-181-1/+4
| | | | | | | setShadowHidden is async and hits Supabase. The PUT handler called it without await, so the handler could respond before the database write landed (and any error was silently lost). Add the missing await so the response only goes out after the update settles.
* fix(api): drop unused redirect query param from oauth refreshFuwn2026-04-181-3/+1
| | | | | | | | | | | | The refresh endpoint accepted a ?redirect query param and, when present, called redirect(303, "/") instead of returning the refreshed token as JSON. The target was hardcoded to "/" regardless of the param's value, so the feature was dead — and the pattern of reading a "redirect" param invited future open-redirect bugs if someone wired the value through to redirect() directly. The sole in-tree caller (feeds/activity-notifications) reads the JSON response, so always return JSON and drop the redirect import.
* fix(easter-event): drop dead resize/scroll listener cleanupFuwn2026-04-181-3/+0
| | | | | | | The onMount cleanup removed resize and scroll listeners that were never actually registered — the only live side effect is the updatePosition interval. Drop the two dead removeEventListener calls so the cleanup only matches what the component actually sets up.
* fix(media): guard publicMediaListCollection against missing AniList dataFuwn2026-04-181-17/+18
| | | | | | | | | | | | The public variant of mediaListCollection chained data.MediaListCollection .lists with no null check, so any AniList error response (rate limit, private list, nonexistent user) threw a TypeError. Mirror the guard used by the authenticated sibling: if any of data, MediaListCollection, or lists is missing, return an empty array. The function currently has no in-tree callers; noting that it may be a candidate for removal in a follow-up.
* fix(user): pass actual description to badge preview alt textFuwn2026-04-181-1/+1
| | | | | | | | | | The ParallaxImage alternativeText prop was passed as the literal string "selectedBadge.description" (quoted attribute syntax) instead of the expression, so every badge image rendered with that identifier name as its alt text. Switch to the expression form with a nullish-coalescing fallback so badges without a description render empty alt rather than the literal.
* fix(schedule): stop duplicating first page in season paginationFuwn2026-04-181-37/+18
| | | | | | | | | | | | | The seasonal schedule collector pushed page 1 outside the while-loop, then re-pushed the current page on every loop iteration. That meant every multi-page season duplicated its first page's media, and the same bug ran a second time when includeLastSeason was set. Restructure into a shared collectAllSchedulePages helper with a single push-then-check-hasNextPage loop, so each page is pushed exactly once. Verified against AniList for SPRING 2026 with includeLastSeason=true: 210 media, 0 duplicates.
* fix(cdn): preserve upstream headers alongside CORS and cache overridesFuwn2026-04-181-6/+8
| | | | | | | | | | | The response was built with `{ "Cache-Control": ..., "Access-Control- Allow-Origin": ..., ...response.headers }`. Spreading a Headers instance into a plain object does not expand into own properties, so upstream headers (including Content-Type) were dropped on the floor. Build a Headers copy of the upstream response and .set() the overrides on it, so Content-Type and friends survive alongside the locked-down CORS origin and long cache policy.
* fix(api): gate badge click-count on Origin and fix 401 response reuseFuwn2026-04-181-9/+11
| | | | | | | | | | | | | The PUT ?incrementClickCount path ran before any auth guard, letting unauthenticated callers spam-increment arbitrary badges. Require the request Origin to match appOrigin() so legitimate in-browser clicks (authenticated or not) still count while direct scripted calls are rejected. Also convert the shared `unauthorised` Response singleton into a factory. The singleton's body was consumed on first use, so subsequent 401 paths returned a `Response body is locked` error instead of the intended "Unauthorised" body.
* fix(api): encode subsplease timezone to prevent query-param injectionFuwn2026-04-181-5/+6
| | | | | | | The `tz` query value was interpolated raw into the upstream URL, letting callers append arbitrary query segments (e.g. `tz=foo&f=hax`). Wrap the value in encodeURIComponent and rename the local variable away from the banned `tz` abbreviation.
* fix(utility): treat .localhost subdomains as private in appOriginFuwn2026-04-181-0/+1
|
* fix(dev): route portless .localhost URL through app origin and proxy CORSFuwn2026-04-182-1/+2
|
* build(dev): use portless for named .localhost dev URLFuwn2026-04-182-1/+13
|
* fix(ui): hide list count badge when no media is displayedFuwn2026-04-172-2/+2
| | | | | | Showing "0" in the list title next to a "No anime/manga to display" empty-state message duplicates information. Treat an empty media list the same as dummy mode and suppress the count.
* fix(ui): reset media list filter when its custom list disappearsFuwn2026-04-172-0/+20
| | | | | | | | If a user's persisted filter references a custom list that no longer exists on any media (renamed or deleted on AniList), the select falls back to showing "All" but the underlying state still filters to an empty set. Detect the stale value and reset it, clearing the StateBin entry so the fix survives a reload.
* fix(ui): hide follow-fix toggle link until a username is enteredFuwn2026-04-171-3/+5
| | | | | | Previously the link rendered "Toggle follow for ..." and was clickable with an empty submission. Only render once the input has content so there is no dead affordance.
* fix(ui): hide media roulette button when only one item is pickableFuwn2026-04-172-2/+2
| | | | | | Rouletting a single item is deterministic, so the affordance adds nothing. Require at least two items before showing the button on both anime and manga lists.
* fix(ui): hide media list filter when every list matches "All"Fuwn2026-04-172-2/+10
| | | | | | If every custom list contains every displayed media item, each option yields the same result, so the picker is noise. Gate the select on a reactive check that any list excludes at least one item.
* Merge pull request #3 from Fuwn/feat/instant-cache-invalidationFuwn2026-04-1411-34/+111
|\ | | | | feat(cache): instant list revalidation from command palette and debug menu
| * feat(cache): instant list revalidation from command palette and debug menuFuwn2026-04-1511-52/+106
| | | | | | | | | | | | | | | | | | Replaces one-shot boolean revalidateAnime with a counter store so multiple components can subscribe without race conditions. Adds revalidateManga store and reactive refresh blocks to DueAnimeList and MangaListTemplate. Extracts shared invalidateListCaches function used by both the command palette action and the debug settings button. Renames palette item to "Refresh" and debug button to "Invalidate".
| * feat(command-palette): add clear anime and manga list caches actionFuwn2026-04-151-0/+23
|/
* fix(ui): balance homepage media panelsFuwn2026-04-121-62/+220
|
* fix(settings): clarify data saver checkboxFuwn2026-04-024-0/+6
|
* feat(settings): add checkbox tooltipsFuwn2026-04-021-0/+5
|
* fix(ui): stop roulette at intended durationFuwn2026-04-021-1/+2
|
* fix(ui): correct manga list tooltip behaviorFuwn2026-04-022-2/+8
|
* fix(ui): polish media list title separatorsFuwn2026-04-021-0/+8
|
* fix(ui): align media list title timingsFuwn2026-04-021-2/+3
|
* fix(ui): refine media list title countsFuwn2026-04-021-4/+17
|
* feat(debug): add media list timing toggleFuwn2026-04-026-1/+12
|