fc3g(ui): BrowserExtensionCard on Settings → Maintenance — one-click Firefox install + key copy/rotate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<v-card class="fc-ext-card">
|
||||||
|
<v-card-title class="d-flex align-center" style="gap: 10px;">
|
||||||
|
<v-icon icon="mdi-puzzle" size="small" />
|
||||||
|
<span>Browser extension</span>
|
||||||
|
<span v-if="manifest?.installed" class="text-caption fc-muted">
|
||||||
|
· Firefox · v{{ manifest.version }}
|
||||||
|
</span>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<p class="fc-muted text-body-2">
|
||||||
|
Pushes session cookies from supported platforms
|
||||||
|
(patreon, subscribestar, hentaifoundry, discord, pixiv, deviantart)
|
||||||
|
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">
|
||||||
|
<v-btn
|
||||||
|
v-if="isFirefox"
|
||||||
|
color="accent" variant="flat" rounded="pill"
|
||||||
|
prepend-icon="mdi-firefox"
|
||||||
|
@click="installXpi"
|
||||||
|
>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 { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useApi } from '../../composables/useApi.js'
|
||||||
|
|
||||||
|
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 = ''
|
||||||
|
window.__fcToast?.({
|
||||||
|
text: `Failed to load extension API key: ${e.message}`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installXpi() {
|
||||||
|
if (!manifest.value?.latest_url) return
|
||||||
|
window.location.assign(manifest.value.latest_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rotateKey() {
|
||||||
|
rotating.value = true
|
||||||
|
try {
|
||||||
|
const { key } = await api.post('/api/settings/extension_api_key/rotate')
|
||||||
|
apiKey.value = key
|
||||||
|
keyShown.value = true
|
||||||
|
window.__fcToast?.({ text: 'Extension API key rotated.', type: 'success' })
|
||||||
|
} catch (e) {
|
||||||
|
window.__fcToast?.({
|
||||||
|
text: `Rotate failed: ${e.message}`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
rotating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copy(text, label) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
window.__fcToast?.({ text: `${label} copied.`, type: 'success' })
|
||||||
|
} catch (e) {
|
||||||
|
window.__fcToast?.({ 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;
|
||||||
|
}
|
||||||
|
.fc-muted {
|
||||||
|
color: rgb(var(--v-theme-on-surface-variant));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<MLThresholdSliders class="mt-4" />
|
<MLThresholdSliders class="mt-4" />
|
||||||
<AllowlistTable class="mt-4" />
|
<AllowlistTable class="mt-4" />
|
||||||
<AliasTable class="mt-4" />
|
<AliasTable class="mt-4" />
|
||||||
|
<BrowserExtensionCard class="mt-6" />
|
||||||
<LegacyMigrationCard class="mt-6" />
|
<LegacyMigrationCard class="mt-6" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -22,6 +23,7 @@ import CentroidRecomputeCard from './CentroidRecomputeCard.vue'
|
|||||||
import MLThresholdSliders from './MLThresholdSliders.vue'
|
import MLThresholdSliders from './MLThresholdSliders.vue'
|
||||||
import AllowlistTable from './AllowlistTable.vue'
|
import AllowlistTable from './AllowlistTable.vue'
|
||||||
import AliasTable from './AliasTable.vue'
|
import AliasTable from './AliasTable.vue'
|
||||||
|
import BrowserExtensionCard from './BrowserExtensionCard.vue'
|
||||||
import LegacyMigrationCard from './LegacyMigrationCard.vue'
|
import LegacyMigrationCard from './LegacyMigrationCard.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user