587 lines
17 KiB
Vue
587 lines
17 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 concurrent downloads (1-10). Requires container restart to take effect."
|
|
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="scheduleIntervalHint"
|
|
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>{{ systemInfo.version }}</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>Python Version</strong></td>
|
|
<td>{{ systemInfo.python_version }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Gallery-dl Version</strong></td>
|
|
<td>
|
|
<v-chip color="primary" size="small" variant="tonal">
|
|
{{ systemInfo.gallery_dl_version }}
|
|
</v-chip>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Download Path</strong></td>
|
|
<td>{{ systemInfo.download_path }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Config Path</strong></td>
|
|
<td>{{ systemInfo.config_path }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
|
|
<v-divider class="my-4" />
|
|
|
|
<!-- Dependencies Section -->
|
|
<h4 class="text-subtitle-1 mb-3">
|
|
<v-icon class="mr-1" size="small">mdi-package-variant</v-icon>
|
|
Dependencies
|
|
</h4>
|
|
|
|
<v-table density="compact" class="mb-4">
|
|
<thead>
|
|
<tr>
|
|
<th width="200">Package</th>
|
|
<th>Version</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(version, name) in systemInfo.key_packages" :key="name">
|
|
<td><code>{{ name }}</code></td>
|
|
<td>
|
|
<v-chip size="x-small" variant="outlined">
|
|
{{ version }}
|
|
</v-chip>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
|
|
<v-btn
|
|
variant="text"
|
|
size="small"
|
|
@click="showAllPackages = !showAllPackages"
|
|
>
|
|
<v-icon start>{{ showAllPackages ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
|
{{ showAllPackages ? 'Hide' : 'Show' }} all packages ({{ Object.keys(systemInfo.all_packages || {}).length }})
|
|
</v-btn>
|
|
|
|
<v-expand-transition>
|
|
<div v-if="showAllPackages">
|
|
<v-table density="compact" class="mt-3">
|
|
<thead>
|
|
<tr>
|
|
<th width="250">Package</th>
|
|
<th>Version</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(version, name) in sortedAllPackages" :key="name">
|
|
<td><code class="text-caption">{{ name }}</code></td>
|
|
<td class="text-caption">{{ version }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
</div>
|
|
</v-expand-transition>
|
|
</v-card-text>
|
|
</v-card>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, 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)
|
|
const systemInfo = ref({
|
|
version: '...',
|
|
python_version: '...',
|
|
gallery_dl_version: '...',
|
|
key_packages: {},
|
|
download_path: '...',
|
|
config_path: '...',
|
|
})
|
|
const showAllPackages = ref(false)
|
|
|
|
const sortedAllPackages = computed(() => {
|
|
const packages = systemInfo.value.all_packages || {}
|
|
const sorted = {}
|
|
Object.keys(packages).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())).forEach(key => {
|
|
sorted[key] = packages[key]
|
|
})
|
|
return sorted
|
|
})
|
|
|
|
const scheduleIntervalHint = computed(() => {
|
|
const seconds = settings.value['download.schedule_interval'] || 3600
|
|
if (seconds < 60) return `Check every ${seconds} seconds`
|
|
if (seconds < 3600) return `Check every ${Math.round(seconds / 60)} minutes`
|
|
if (seconds < 86400) {
|
|
const hours = seconds / 3600
|
|
return `Check every ${hours % 1 === 0 ? hours : hours.toFixed(1)} hours`
|
|
}
|
|
const days = seconds / 86400
|
|
return `Check every ${days % 1 === 0 ? days : days.toFixed(1)} days`
|
|
})
|
|
|
|
onMounted(() => {
|
|
// Don't block UI - load data in background
|
|
loadAll()
|
|
})
|
|
|
|
async function loadAll() {
|
|
loading.value = true
|
|
try {
|
|
await Promise.all([
|
|
loadSettings(),
|
|
loadGalleryDLConfig(),
|
|
loadPlatforms(),
|
|
checkApiHealth(),
|
|
loadSystemInfo(),
|
|
])
|
|
} 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
|
|
}
|
|
}
|
|
|
|
async function loadSystemInfo() {
|
|
try {
|
|
const response = await api.get('/settings/system-info')
|
|
systemInfo.value = response.data
|
|
} catch (error) {
|
|
console.error('Failed to load system info:', error)
|
|
}
|
|
}
|
|
|
|
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>
|