Compare commits

...
6 Commits
Author SHA1 Message Date
bvandeusen 89c83ee5de Merge pull request 'fix(extension): Add-to-FC button on subscribed Patreon creators + v1.0.8 (#1485)' (#230) from dev into main
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
extension / lint (push) Successful in 8s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m46s
Build images / sign-extension (push) Successful in 5m8s
Build images / build-web (push) Successful in 10s
2026-07-13 17:56:32 -04:00
bvandeusenandClaude Opus 4.8 69b5637bd6 fix(extension): show Add-to-FC button on subscribed Patreon creators (#1485)
CI / lint (push) Successful in 3s
extension / lint (push) Successful in 11s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m47s
extension / lint (pull_request) Successful in 9s
Symptom (operator-flagged): the extension injected the Add-as-source button on
a Patreon creator you had NOT subscribed to, but it disappeared once you were
subscribed — the opposite of when it's useful.

Root cause (extension logic, not Patreon security): Patreon serves a creator
under three URL shapes — bare patreon.com/Atole, patreon.com/c/Atole, and
patreon.com/cw/Atole (the 'creator workspace' URL you land on once subscribed;
documented in patreon_resolver._VANITY_RE). The button's artist-page gate
(PLATFORM_ARTIST_PATTERNS.patreon in platforms.js) and its byte-mirror probe
pattern (_PLATFORM_PATTERNS in extension_service._derive) only matched the bare
single-segment form and explicitly excluded c/. So the subscribed-view URL
failed the gate → no button. The ingestion resolver already handled all three;
only these two gates were too narrow.

Fix: both regexes now accept optional cw/ and c/ prefixes and drop the strict
single-segment end-anchor, so a creator's inner page (/cw/Atole/posts,
/Atole/membership) also matches — robust to whatever exact shape the subscribed
view uses. Nav-page exclusions (home/search/messages/notifications/library/
settings/posts + post permalinks) preserved. New unit test covers all three
prefixes, sub-paths, and nav-page rejection (both regexes validated identically).

Bump extension 1.0.7→1.0.8 so a fresh signed XPI ships the fix (also exercises
batch-5 web-ext-10's AMO sign path end-to-end on the main build).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:50:07 -04:00
bvandeusen d3192f1843 Merge pull request 'chore(deps): web-ext 8→10 (Renovate batch 5) (#1450)' (#229) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 6s
extension / lint (push) Successful in 9s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m48s
2026-07-13 16:36:26 -04:00
bvandeusenandClaude Opus 4.8 51749e05db chore(deps): update web-ext 8→10 (batch 5) (#1450)
CI / lint (push) Successful in 2s
extension / lint (push) Successful in 9s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m58s
extension / lint (pull_request) Successful in 9s
Renovate dep-dashboard batch 5. web-ext is the extension's build/lint/sign
CLI (devDependency only; no extension source changes).

Verified against the 8→10 changelog + FC's actual usage:
- All CLI flags we use survive unchanged: --source-dir, --no-config-discovery,
  --ignore-files, --overwrite-dest (build), --channel/--api-key/--api-secret
  (sign). No removed/renamed flags for lint/build/sign.
- v9's one breaking change (.js config files rejected) does NOT apply: we pass
  --no-config-discovery on every command and ship no config file.
- Node: web-ext 10 baselines Node 22. The lint job runs on node:24-bookworm-slim;
  the load-bearing AMO sign job runs on ci-python:3.14 which installs Node 24
  (CI-runner NODE_MAJOR=24) — both satisfy it. Sign is cache-skipped this push
  (extension version unchanged at 1.0.7) but is verified compatible for the next
  version bump.
- The bundled addons-linter jumps to 10.1.0 — the extension.yml lint job (web-ext
  lint over the MV3 manifest) is the CI verifier for any new manifest findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:30:11 -04:00
bvandeusen 5a5694f200 Merge pull request 'fix(ui): softer chrome-gradient falloff + segmented media toggle (#1478)' (#228) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m42s
2026-07-13 15:55:02 -04:00
bvandeusenandClaude Opus 4.8 50d6c42207 fix(ui): softer chrome-gradient falloff + segmented media toggle (#1478)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m49s
Two operator-flagged polish items from the Vuetify-4 review:

Gradient: reshape the nav + sub-header fade from a near-linear ramp to a
hold-then-soft-drop profile — hold high opacity (0.92 → seam 0.68) through the
bulk of the chrome, then ease to transparent over a small section at the bottom
with an intermediate stop, so it tails off softly instead of running a straight
line into a hard edge. Raising the shared --fc-chrome-seam also makes the tab
strips more legible over scrolling content.

Media toggle: FC's global VBtn { rounded: 'pill' } default made Vuetify 4
pill-round each SEGMENT of the All/Images/Videos v-btn-toggle individually, so
the rounded ends collided at the joins. Square the inner segments and clip the
group to one 8px outline — a proper segmented control.

Both are colour/border-radius only — no control height changes, so the filter
bar height and the nav offset (--fc-nav-h) are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:50:33 -04:00
8 changed files with 123 additions and 12 deletions
+7 -2
View File
@@ -35,9 +35,14 @@ class InvalidUrlError(Exception):
# reviewers catch drift. # reviewers catch drift.
_PLATFORM_PATTERNS: list[tuple[str, re.Pattern[str]]] = [ _PLATFORM_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
("patreon", re.compile( ("patreon", re.compile(
# Three creator URL shapes — bare (patreon.com/Atole), `c/`, and `cw/`
# (the "creator workspace" URL served once subscribed, see
# patreon_resolver._VANITY_RE). A trailing sub-path is allowed so a
# creator's inner page still derives the slug. Nav pages stay excluded.
r"^https?://(?:www\.)?patreon\.com/" r"^https?://(?:www\.)?patreon\.com/"
r"(?!home$|search\b|messages\b|notifications\b|library\b|settings\b|posts\b|c/)" r"(?:cw/|c/)?"
r"(?P<slug>[^/?#]+)/?$", r"(?!(?:home|search|messages|notifications|library|settings|posts)(?:[/?#]|$))"
r"(?P<slug>[^/?#]+)",
re.IGNORECASE, re.IGNORECASE,
)), )),
("subscribestar", re.compile( ("subscribestar", re.compile(
+10 -1
View File
@@ -86,7 +86,16 @@ const PLATFORMS = {
* script to decide whether to show the floating "Add as source" button. * script to decide whether to show the floating "Add as source" button.
*/ */
const PLATFORM_ARTIST_PATTERNS = { const PLATFORM_ARTIST_PATTERNS = {
patreon: /^https?:\/\/(www\.)?patreon\.com\/(?!home$|search\b|messages\b|notifications\b|library\b|settings\b|posts\b|c\/)[^/?#]+\/?$/i, // Patreon serves the same creator under three URL shapes (see backend
// patreon_resolver._VANITY_RE): bare `patreon.com/Atole`, `c/` prefix, and
// `cw/` "creator workspace" — the last is the URL you land on once you're
// SUBSCRIBED, which is exactly when the button matters. Match all three, and
// drop the single-segment end-anchor so a creator's inner page
// (…/cw/Atole/posts, …/Atole/membership) also injects the button. Nav pages
// (home/search/…/posts permalink) stay excluded. Mirrors extension_service
// ._PLATFORM_PATTERNS — keep in sync (operator-flagged 2026-07-13: button
// vanished once subscribed because the old pattern only matched the bare root).
patreon: /^https?:\/\/(www\.)?patreon\.com\/(?:cw\/|c\/)?(?!(?:home|search|messages|notifications|library|settings|posts)(?:[\/?#]|$))[^/?#]+/i,
subscribestar: /^https?:\/\/(www\.)?subscribestar\.(com|adult)\/(?!feed$|messages$|library$)[^/?#]+\/?$/i, subscribestar: /^https?:\/\/(www\.)?subscribestar\.(com|adult)\/(?!feed$|messages$|library$)[^/?#]+\/?$/i,
hentaifoundry: /^https?:\/\/(www\.)?hentai-foundry\.com\/user\/[^/?#]+/i, hentaifoundry: /^https?:\/\/(www\.)?hentai-foundry\.com\/user\/[^/?#]+/i,
deviantart: /^https?:\/\/(www\.)?deviantart\.com\/(?!home$|watch\b|tag\b|browse\b)[^/?#]+\/?$/i, deviantart: /^https?:\/\/(www\.)?deviantart\.com\/(?!home$|watch\b|tag\b|browse\b)[^/?#]+\/?$/i,
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "FabledCurator", "name": "FabledCurator",
"version": "1.0.7", "version": "1.0.8",
"description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.", "description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.",
"browser_specific_settings": { "browser_specific_settings": {
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "fabledcurator-extension", "name": "fabledcurator-extension",
"version": "1.0.7", "version": "1.0.8",
"private": true, "private": true,
"description": "Firefox extension for FabledCurator", "description": "Firefox extension for FabledCurator",
"scripts": { "scripts": {
@@ -10,6 +10,6 @@
"sign": "web-ext sign --source-dir=. --no-config-discovery --ignore-files package.json package-lock.json web-ext-artifacts node_modules README.md .gitignore --channel=unlisted --api-key=$WEB_EXT_API_KEY --api-secret=$WEB_EXT_API_SECRET" "sign": "web-ext sign --source-dir=. --no-config-discovery --ignore-files package.json package-lock.json web-ext-artifacts node_modules README.md .gitignore --channel=unlisted --api-key=$WEB_EXT_API_KEY --api-secret=$WEB_EXT_API_SECRET"
}, },
"devDependencies": { "devDependencies": {
"web-ext": "^8.0.0" "web-ext": "^10.0.0"
} }
} }
+10 -5
View File
@@ -145,12 +145,17 @@ const health = computed(() => {
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
/* Obsidian (#14171A = 20,23,26) gradient fade — content scrolls under it. */ /* Obsidian (#14171A) fade — content scrolls under it. Holds high (0.92 →
0.84) through the top half, then eases to transparent over the bottom
quarter so it tails off softly instead of a straight line to a hard edge
(operator 2026-07-13). Shared --fc-chrome-rgb keeps it in sync with the
sub-header continuation. */
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
rgba(20, 23, 26, 0.92) 0%, rgba(var(--fc-chrome-rgb), 0.92) 0%,
rgba(20, 23, 26, 0.65) 60%, rgba(var(--fc-chrome-rgb), 0.84) 50%,
rgba(20, 23, 26, 0) 100% rgba(var(--fc-chrome-rgb), 0.55) 75%,
rgba(var(--fc-chrome-rgb), 0) 100%
); );
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
@@ -165,7 +170,7 @@ const health = computed(() => {
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
rgba(var(--fc-chrome-rgb), 0.92) 0%, rgba(var(--fc-chrome-rgb), 0.92) 0%,
rgba(var(--fc-chrome-rgb), 0.72) 55%, rgba(var(--fc-chrome-rgb), 0.84) 60%,
rgba(var(--fc-chrome-rgb), var(--fc-chrome-seam)) 100% rgba(var(--fc-chrome-rgb), var(--fc-chrome-seam)) 100%
); );
} }
@@ -331,6 +331,20 @@ function pushFilter(mutate) {
.fc-filterbar-wrap :deep(.v-btn-group) { .fc-filterbar-wrap :deep(.v-btn-group) {
background-color: rgba(20, 23, 26, 0.72); background-color: rgba(20, 23, 26, 0.72);
} }
/* Media toggle (All / Images / Videos) as ONE cohesive segmented control.
FC's global VBtn { rounded: 'pill' } default made Vuetify 4 pill-round each
SEGMENT individually, so the rounded ends collided at the joins — the shapes
landed awkwardly on the button edges (operator 2026-07-13). Square the inner
segments (over the pill utility's !important) and clip the group to a single
8px outline (matches the chips/tiles rounding elsewhere in the app). Radius
only — no height change, so the bar height and nav offset are untouched. */
.fc-filterbar-wrap :deep(.v-btn-toggle) {
border-radius: 8px;
overflow: hidden;
}
.fc-filterbar-wrap :deep(.v-btn-toggle .v-btn) {
border-radius: 0 !important;
}
.fc-filterbar__search { max-width: 320px; min-width: 200px; } .fc-filterbar__search { max-width: 320px; min-width: 200px; }
.fc-filterbar__chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } .fc-filterbar__chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
/* The tag chips' bodies toggle include/exclude — signal they're clickable. */ /* The tag chips' bodies toggle include/exclude — signal they're clickable. */
+14 -1
View File
@@ -77,7 +77,13 @@
legible tabs/controls over scrolling content), lower it for a lighter fade. */ legible tabs/controls over scrolling content), lower it for a lighter fade. */
:root { :root {
--fc-chrome-rgb: 20, 23, 26; /* obsidian #14171A — matches the TopNav */ --fc-chrome-rgb: 20, 23, 26; /* obsidian #14171A — matches the TopNav */
--fc-chrome-seam: 0.46; /* alpha where the nav hands off to the sub-header */ /* Alpha where the nav hands off to the sub-header — also the "hold" level of
the fade. The chrome stays fairly opaque (0.92 → this) through the bulk of
its height, then drops to transparent in a small eased section at the very
bottom (see the multi-stop gradients), so it reads as a slow falloff that
tails off softly rather than a straight line to a hard edge (operator
2026-07-13). Raise for heavier/more-legible chrome, lower for a lighter fade. */
--fc-chrome-seam: 0.68;
/* Actual TopNav height, measured live (ResizeObserver in TopNav.vue) and used /* Actual TopNav height, measured live (ResizeObserver in TopNav.vue) and used
by full-height workspaces (Explore/Subscriptions: calc(100vh - var)) and by by full-height workspaces (Explore/Subscriptions: calc(100vh - var)) and by
every sticky sub-header pinned beneath the nav (top: var). This was a every sticky sub-header pinned beneath the nav (top: var). This was a
@@ -92,9 +98,16 @@
controls legible as the fill thins toward transparent — the solid-surface controls legible as the fill thins toward transparent — the solid-surface
bars it replaces had none, so it must live here. */ bars it replaces had none, so it must live here. */
.fc-chrome-continues { .fc-chrome-continues {
/* Continues the nav's fade: HOLDS near the seam alpha through the first ~55%
(subtle), then eases down to transparent over the last ~45% with an
intermediate stop so the tail is soft — no hard line at the bottom edge
(operator 2026-07-13). Percentage stops keep the shape spanning the
element's height (survives the filter bar's expanding refine panel). */
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
rgba(var(--fc-chrome-rgb), var(--fc-chrome-seam)) 0%, rgba(var(--fc-chrome-rgb), var(--fc-chrome-seam)) 0%,
rgba(var(--fc-chrome-rgb), 0.60) 55%,
rgba(var(--fc-chrome-rgb), 0.28) 82%,
rgba(var(--fc-chrome-rgb), 0) 100% rgba(var(--fc-chrome-rgb), 0) 100%
); );
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
+65
View File
@@ -0,0 +1,65 @@
"""Unit tests for ExtensionService._derive — the URL → (platform, slug)
parser that gates the browser extension's "Add as source" button and pulls
the creator slug on probe/add.
Regression cover for #1485: Patreon serves the same creator under three URL
shapes — bare `patreon.com/Atole`, `c/`, and `cw/` (the "creator workspace"
URL you land on once SUBSCRIBED). The button used to vanish while subscribed
because the pattern only matched the bare root and excluded `c/`.
_derive is pure URL parsing (no DB / no async), so a session-less instance is
fine to exercise directly.
"""
import pytest
from backend.app.services.extension_service import (
ExtensionService,
InvalidUrlError,
UnknownPlatformError,
)
_svc = ExtensionService(None)
@pytest.mark.parametrize(
"url, slug",
[
# All three Patreon creator prefixes resolve to the same vanity slug.
("https://www.patreon.com/Atole", "Atole"),
("https://www.patreon.com/c/Atole", "Atole"),
("https://www.patreon.com/cw/Atole", "Atole"), # subscribed-view URL
# A creator's inner page still derives the slug (trailing sub-path).
("https://www.patreon.com/cw/Atole/posts", "Atole"),
("https://www.patreon.com/Atole/membership", "Atole"),
("https://patreon.com/c/Atole", "Atole"), # bare host, no www
],
)
def test_derive_patreon_creator_urls(url, slug):
platform, got = _svc._derive(url)
assert platform == "patreon"
assert got == slug
@pytest.mark.parametrize(
"url",
[
# Patreon's own nav pages must never read as a creator slug.
"https://www.patreon.com/home",
"https://www.patreon.com/settings",
"https://www.patreon.com/search",
"https://www.patreon.com/messages",
"https://www.patreon.com/library",
"https://www.patreon.com/notifications",
"https://www.patreon.com/posts/12345", # post permalink
"https://www.patreon.com/settings/billing", # nav sub-page
],
)
def test_derive_patreon_nav_pages_rejected(url):
with pytest.raises(UnknownPlatformError):
_svc._derive(url)
def test_derive_rejects_missing_scheme():
with pytest.raises(InvalidUrlError):
_svc._derive("patreon.com/Atole")