Files
FabledCurator/frontend/src/components/settings/BrowserExtensionCard.vue
T
bvandeusenandClaude Opus 5 ddf896078c
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 23s
extension / lint (push) Successful in 26s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m43s
refactor(platforms): retire deviantart end-to-end (#3069)
Executes the 2026-07-05 product decision (FC downloaders = art-dedicated
services only), which removed Twitter/X and Bluesky but left deviantart
fully wired for seven weeks — the half-retired state rule 22 exists to
prevent.

Removed: the PlatformInfo module and its registry entry, the gallery-dl
extractor block, extension_service's artist-page pattern, the extension's
PLATFORMS + PLATFORM_ARTIST_PATTERNS entries, its manifest host permission
and content-script match, the frontend icon/colour/label, and the operator-
facing "supported platforms" list that still advertised it.

Two judgment calls, both recorded in migration 0088:

  * existing `source` rows are DISABLED, not deleted. The row is the only
    record of the artist's DeviantArt URL. Disabling is also required for
    correctness rather than tidiness: with the platform unregistered the
    download path falls through to gallery-dl, which carries its OWN
    deviantart extractor, so an enabled row would have kept downloading
    from a dropped platform.
  * the `credential` row IS deleted — a live session cookie for a site FC
    will never call again.

Adds the invariant whose absence is why manifest.json drifted in the first
place: nothing tied its domain lists back to the platform table. The
extension suite now asserts both directions, plus that no host permission
belongs to an unclaimed domain (`*://*/*` exempted — FC is self-hosted at
an operator-chosen URL the extension cannot enumerate).

Extension version 1.0.10 -> 1.0.11: ci.yml's guard hard-fails a packaged
extension change without a bump. No release is cut — build.yml's
sign-extension job only runs on main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 07:24:08 -04:00

190 lines
5.7 KiB
Vue

<template>
<v-card class="fc-ext-card">
<CardHeading icon="mdi-puzzle" title="Browser extension">
<span v-if="manifest?.installed" class="text-caption fc-muted">
· Firefox · v{{ manifest.version }}
</span>
</CardHeading>
<v-card-text>
<p class="fc-muted text-body-2">
Pushes session cookies from supported platforms
(patreon, subscribestar, hentaifoundry, discord, pixiv)
into FabledCurator, and lets you add a creator as a source from
their page in one click.
</p>
<v-alert
v-if="manifestError"
type="warning" variant="tonal" density="compact" class="mt-3"
>
Could not load extension manifest: {{ manifestError }}
</v-alert>
<v-alert
v-else-if="manifest && !manifest.installed"
type="warning" variant="tonal" density="compact" class="mt-3"
>
No bundled extension found in this image. Push a release that
runs the extension CI workflow, or grab the XPI from the
FabledCurator Forgejo releases page.
</v-alert>
<template v-else-if="manifest?.installed">
<div class="fc-ext-install mt-3">
<!-- Install button: direct :href anchor click (no programmatic
window.location.assign). Firefox's XPI-install gesture
requires a user-clicked anchor pointing at an
application/x-xpinstall response; programmatic navigation
sometimes triggered nothing instead of the install dialog
(operator-flagged 2026-05-26). No `download` attribute —
that would force a save dialog instead of install. -->
<v-btn
v-if="isFirefox"
color="accent" variant="flat" rounded="pill"
prepend-icon="mdi-firefox"
:href="manifest.latest_url"
>Install Firefox extension</v-btn>
<v-btn
variant="outlined" rounded="pill"
:href="manifest.latest_url" download
prepend-icon="mdi-download"
>Download XPI</v-btn>
<v-alert
v-if="!isFirefox"
type="info" variant="tonal" density="compact" class="mt-3"
>
Open this page in Firefox to install in one click, or use
"Download XPI" to install manually.
</v-alert>
</div>
<v-divider class="my-4" />
<div class="fc-muted text-body-2 mb-3">
After installing, open the extension's options page
(about:addons FabledCurator Preferences) and paste these:
</div>
<v-text-field
label="FC base URL" :model-value="apiUrl"
readonly density="compact" hide-details
append-inner-icon="mdi-content-copy"
@click:append-inner="copy(apiUrl, 'URL')"
/>
<v-text-field
label="Extension API key"
:model-value="apiKey"
:type="keyShown ? 'text' : 'password'"
readonly density="compact" hide-details class="mt-3"
>
<template #append-inner>
<v-btn
variant="text" density="compact" size="small"
:icon="keyShown ? 'mdi-eye-off' : 'mdi-eye'"
@click="keyShown = !keyShown"
/>
<v-btn
variant="text" density="compact" size="small"
icon="mdi-content-copy"
@click="copy(apiKey, 'API key')"
/>
<v-btn
variant="text" density="compact" size="small"
icon="mdi-refresh" color="warning"
:loading="rotating"
@click="rotateKey"
/>
</template>
</v-text-field>
</template>
</v-card-text>
</v-card>
</template>
<script setup>
import { toast } from '../../utils/toast.js'
import { computed, onMounted, ref } from 'vue'
import { useApi } from '../../composables/useApi.js'
import { copyText } from '../../utils/clipboard.js'
import CardHeading from '../common/CardHeading.vue'
const api = useApi()
const manifest = ref(null)
const manifestError = ref(null)
const apiKey = ref('')
const keyShown = ref(false)
const rotating = ref(false)
const apiUrl = computed(() => `${window.location.origin}/api`)
const isFirefox = computed(() => navigator.userAgent.includes('Firefox'))
onMounted(async () => {
await Promise.all([loadManifest(), loadKey()])
})
async function loadManifest() {
try {
manifest.value = await api.get('/api/extension/manifest')
} catch (e) {
if (e.status === 404) {
// Backend says no XPI is bundled — surface the not-installed
// state, not an error.
manifest.value = { installed: false }
} else {
manifestError.value = e.message
}
}
}
async function loadKey() {
try {
const { key } = await api.get('/api/settings/extension_api_key')
apiKey.value = key
} catch (e) {
apiKey.value = ''
toast({
text: `Failed to load extension API key: ${e.message}`,
type: 'error',
})
}
}
async function rotateKey() {
rotating.value = true
try {
const { key } = await api.post('/api/settings/extension_api_key/rotate')
apiKey.value = key
keyShown.value = true
toast({ text: 'Extension API key rotated.', type: 'success' })
} catch (e) {
toast({
text: `Rotate failed: ${e.message}`,
type: 'error',
})
} finally {
rotating.value = false
}
}
async function copy(text, label) {
try {
await copyText(text)
toast({ text: `${label} copied.`, type: 'success' })
} catch (e) {
toast({ text: `Copy failed: ${e.message}`, type: 'error' })
}
}
</script>
<style scoped>
.fc-ext-card { border-radius: 8px; }
.fc-ext-install {
display: flex; flex-wrap: wrap; gap: 8px;
}
</style>