This repository has been archived on 2026-05-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
GallerySubscriber/frontend/src/views/Settings.vue
T
2026-01-24 22:52:51 -05:00

475 lines
14 KiB
Vue

<template>
<div>
<v-card :loading="loading" class="mb-6">
<v-card-title>
<v-icon class="mr-2">mdi-cog</v-icon>
Application Settings
</v-card-title>
<v-card-text>
<v-form ref="settingsForm">
<!-- Download Settings -->
<h3 class="text-h6 mb-4">Download Settings</h3>
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model.number="settings['download.parallel_limit']"
label="Parallel Download Limit"
type="number"
min="1"
max="10"
hint="Maximum number of concurrent downloads (1-10)"
persistent-hint
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model.number="settings['download.rate_limit']"
label="Rate Limit (seconds)"
type="number"
min="0.5"
max="30"
step="0.5"
hint="Delay between downloads to avoid rate limiting"
persistent-hint
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model.number="settings['download.retry_count']"
label="Retry Count"
type="number"
min="0"
max="10"
hint="Number of times to retry failed downloads"
persistent-hint
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model.number="settings['download.schedule_interval']"
label="Schedule Interval (seconds)"
type="number"
min="300"
hint="How often to check for new content (minimum 5 minutes)"
persistent-hint
/>
</v-col>
</v-row>
<v-divider class="my-6" />
<!-- Notification Settings -->
<h3 class="text-h6 mb-4">Notifications</h3>
<v-row>
<v-col cols="12" md="6">
<v-switch
v-model="settings['notification.enabled']"
label="Enable Notifications"
color="primary"
hint="Receive notifications when new content is downloaded"
persistent-hint
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings['notification.webhook_url']"
label="Webhook URL (optional)"
:disabled="!settings['notification.enabled']"
hint="Discord/Slack webhook for push notifications"
persistent-hint
placeholder="https://discord.com/api/webhooks/..."
/>
</v-col>
</v-row>
<v-divider class="my-6" />
<!-- Extension API Key -->
<h3 class="text-h6 mb-4">Extension API Key</h3>
<v-row>
<v-col cols="12">
<v-alert type="info" variant="tonal" class="mb-4">
Use this API key to configure the browser extension. Keep it secret - anyone with this key can add subscriptions to your account.
</v-alert>
<v-text-field
v-model="settings['security.extension_api_key']"
label="API Key"
readonly
:type="showApiKey ? 'text' : 'password'"
:append-inner-icon="showApiKey ? 'mdi-eye-off' : 'mdi-eye'"
@click:append-inner="showApiKey = !showApiKey"
>
<template v-slot:append>
<v-btn
icon
variant="text"
size="small"
@click="copyApiKey"
>
<v-icon>mdi-content-copy</v-icon>
<v-tooltip activator="parent">Copy to clipboard</v-tooltip>
</v-btn>
<v-btn
icon
variant="text"
size="small"
color="warning"
@click="confirmRegenerateKey"
>
<v-icon>mdi-refresh</v-icon>
<v-tooltip activator="parent">Regenerate key</v-tooltip>
</v-btn>
</template>
</v-text-field>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
@click="loadSettings"
:disabled="saving"
>
Reset
</v-btn>
<v-btn
color="primary"
@click="saveSettings"
:loading="saving"
>
Save Settings
</v-btn>
</v-card-actions>
</v-card>
<!-- Gallery-dl Configuration -->
<v-card class="mb-6">
<v-card-title>
<v-icon class="mr-2">mdi-code-json</v-icon>
Gallery-dl Configuration
<v-chip class="ml-2" size="small" color="warning" variant="tonal">
Advanced
</v-chip>
</v-card-title>
<v-card-text>
<v-alert type="warning" variant="tonal" class="mb-4">
This is the raw gallery-dl configuration file. Only modify if you know what you're doing.
Invalid configuration may cause downloads to fail.
</v-alert>
<v-textarea
v-model="galleryDLConfigText"
label="gallery-dl.conf (JSON)"
rows="20"
variant="outlined"
:error-messages="configError"
@input="validateConfig"
class="font-monospace"
/>
</v-card-text>
<v-card-actions>
<v-btn
variant="text"
href="https://github.com/mikf/gallery-dl/blob/master/docs/configuration.rst"
target="_blank"
>
<v-icon start>mdi-open-in-new</v-icon>
Documentation
</v-btn>
<v-spacer />
<v-btn
variant="text"
@click="loadGalleryDLConfig"
:disabled="savingConfig"
>
Reset
</v-btn>
<v-btn
color="primary"
@click="saveGalleryDLConfig"
:loading="savingConfig"
:disabled="!!configError"
>
Save Configuration
</v-btn>
</v-card-actions>
</v-card>
<!-- Platform Default Settings -->
<v-card class="mb-6">
<v-card-title>
<v-icon class="mr-2">mdi-web</v-icon>
Platform Defaults
</v-card-title>
<v-card-text>
<p class="text-body-2 mb-4">
Default settings for each platform. These can be overridden per-source.
</p>
<v-expansion-panels>
<v-expansion-panel
v-for="(info, platform) in platforms"
:key="platform"
>
<v-expansion-panel-title>
<v-icon :color="getPlatformColor(platform)" class="mr-2">
{{ getPlatformIcon(platform) }}
</v-icon>
{{ info.name }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-table density="compact">
<tbody>
<tr>
<td width="200"><strong>Auth Required</strong></td>
<td>
<v-chip :color="info.requires_auth ? 'warning' : 'success'" size="small">
{{ info.requires_auth ? 'Yes' : 'No' }}
</v-chip>
</td>
</tr>
<tr>
<td><strong>Auth Type</strong></td>
<td class="text-capitalize">{{ info.auth_type }}</td>
</tr>
<tr>
<td><strong>Content Types</strong></td>
<td>
<v-chip
v-for="type in info.content_types"
:key="type"
size="small"
class="mr-1 mb-1"
>
{{ type }}
</v-chip>
</td>
</tr>
<tr>
<td><strong>Default Sleep</strong></td>
<td>{{ info.default_config?.sleep || 3.0 }} seconds</td>
</tr>
<tr>
<td><strong>URL Examples</strong></td>
<td>
<div v-for="url in info.url_examples" :key="url" class="text-caption">
{{ url }}
</div>
</td>
</tr>
</tbody>
</v-table>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
</v-card>
<!-- System Information -->
<v-card>
<v-card-title>
<v-icon class="mr-2">mdi-information</v-icon>
System Information
</v-card-title>
<v-card-text>
<v-table density="compact">
<tbody>
<tr>
<td width="200"><strong>Version</strong></td>
<td>1.0.0</td>
</tr>
<tr>
<td><strong>API Status</strong></td>
<td>
<v-chip :color="apiHealthy ? 'success' : 'error'" size="small">
{{ apiHealthy ? 'Connected' : 'Disconnected' }}
</v-chip>
</td>
</tr>
<tr>
<td><strong>Download Path</strong></td>
<td>/data/downloads</td>
</tr>
</tbody>
</v-table>
</v-card-text>
</v-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useSettingsStore } from '../stores/settings'
import { useNotificationStore } from '../stores/notifications'
import { platformsApi } from '../services/api'
import api from '../services/api'
const settingsStore = useSettingsStore()
const notifications = useNotificationStore()
const loading = ref(false)
const saving = ref(false)
const savingConfig = ref(false)
const settings = ref({})
const galleryDLConfigText = ref('')
const configError = ref('')
const platforms = ref({})
const apiHealthy = ref(true)
const showApiKey = ref(false)
const regenerateDialog = ref(false)
onMounted(async () => {
await loadAll()
})
async function loadAll() {
loading.value = true
try {
await Promise.all([
loadSettings(),
loadGalleryDLConfig(),
loadPlatforms(),
checkApiHealth(),
])
} finally {
loading.value = false
}
}
async function loadSettings() {
try {
const data = await settingsStore.fetchSettings()
settings.value = { ...data }
} catch (error) {
notifications.error(`Failed to load settings: ${error.message}`)
}
}
async function saveSettings() {
saving.value = true
try {
await settingsStore.updateSettings(settings.value)
notifications.success('Settings saved')
} catch (error) {
notifications.error(`Failed to save: ${error.message}`)
} finally {
saving.value = false
}
}
async function loadGalleryDLConfig() {
try {
const config = await settingsStore.fetchGalleryDLConfig()
galleryDLConfigText.value = JSON.stringify(config, null, 2)
configError.value = ''
} catch (error) {
notifications.error(`Failed to load config: ${error.message}`)
}
}
function validateConfig() {
try {
JSON.parse(galleryDLConfigText.value)
configError.value = ''
} catch (e) {
configError.value = `Invalid JSON: ${e.message}`
}
}
async function saveGalleryDLConfig() {
if (configError.value) return
savingConfig.value = true
try {
const config = JSON.parse(galleryDLConfigText.value)
await settingsStore.updateGalleryDLConfig(config)
notifications.success('Gallery-dl configuration saved')
} catch (error) {
notifications.error(`Failed to save: ${error.message}`)
} finally {
savingConfig.value = false
}
}
async function loadPlatforms() {
try {
const response = await platformsApi.list()
platforms.value = response.data.platforms
} catch (error) {
console.error('Failed to load platforms:', error)
}
}
async function checkApiHealth() {
try {
await api.get('/health')
apiHealthy.value = true
} catch {
apiHealthy.value = false
}
}
function getPlatformIcon(platform) {
const icons = {
patreon: 'mdi-patreon',
subscribestar: 'mdi-star',
hentaifoundry: 'mdi-palette',
discord: 'mdi-discord',
}
return icons[platform] || 'mdi-web'
}
function getPlatformColor(platform) {
const colors = {
patreon: '#FF424D',
subscribestar: '#FFD700',
hentaifoundry: '#9C27B0',
discord: '#5865F2',
}
return colors[platform] || 'grey'
}
async function copyApiKey() {
try {
await navigator.clipboard.writeText(settings.value['security.extension_api_key'])
notifications.success('API key copied to clipboard')
} catch (error) {
notifications.error('Failed to copy to clipboard')
}
}
async function confirmRegenerateKey() {
if (confirm('Are you sure you want to regenerate the API key? The old key will stop working immediately.')) {
await regenerateApiKey()
}
}
async function regenerateApiKey() {
try {
const response = await api.post('/settings/api-key/regenerate')
settings.value['security.extension_api_key'] = response.data.api_key
notifications.success('API key regenerated')
} catch (error) {
notifications.error(`Failed to regenerate: ${error.message}`)
}
}
</script>
<style scoped>
.font-monospace {
font-family: 'Courier New', Courier, monospace;
}
</style>