9322c984fd
Replaces the three top-level routes with a single `/subscriptions` parent
owning the whole download-pipeline domain. Internal tab state via `?tab=`
query param, mirroring ArtistView's pattern. TopNav auto-drops the two
removed entries (route-driven via meta.title). Bookmark-safe redirects
from `/credentials` and `/downloads` route into the appropriate subtab.
**Subtab 1 — Subscriptions (default).** Carries over the existing
artist-grouped expandable table; adds (a) status filter dropdown, (b)
bulk-select column with Enable/Disable/Delete-all actions, (c) GS-style
color-coded `PlatformChip` per distinct platform in the collapsed row.
Reuses SourceRow, SourceHealthDot, SourceFormDialog, ArtistCreateDialog.
**Subtab 2 — Downloads.** Full GS dashboard. Five colored stat chips up
top (Queued/Running/Completed/Failed/Skipped, sourced from new
`GET /api/downloads/stats?window_hours=`). Popover-style filter UI
(Status/Source/FromDate/ToDate) with active-filter pills below.
Maintenance menu wraps existing /api/import/retry-failed and
/api/import/clear-stuck endpoints; Export-failed-logs item disabled with
a "v2" tooltip. Per-row Retry preserved via existing DownloadEventRow.
**Subtab 3 — Settings.** Four sections: ExtensionKeyBar (top), GS-style
per-platform CredentialCard grid (md=6 v-row/v-col, dashed border if
unset / accent border if set, expandable how-to panel), Downloader card
(rate limit, validate_files), Schedule defaults card (default interval,
event retention, failure warning threshold). The Downloader and Schedule
sections were extracted out of components/settings/ImportFiltersForm.vue
— SettingsView's Import tab now owns only image-import filters.
**Backend:** new `GET /api/downloads/stats` returns
{pending, running, ok, error, skipped} count grouped by status over the
configurable window. Status keys stay raw from the ENUM; UI does the
display-label mapping. Two integration tests pin the response shape +
window_hours validation.
**Util:** `frontend/src/utils/platformColor.js` — single source of truth
for the six platforms' color + icon + label, mirroring GS's palette
(patreon=red mdi-patreon, subscribestar=amber mdi-star,
hentaifoundry=purple mdi-palette, discord=indigo mdi-discord,
pixiv=blue mdi-alpha-p-box, deviantart=green mdi-deviantart). Unknown
platform falls back to grey + mdi-web.
Deferred (explicit non-goals): subscription import/export, "Trigger Due
Now" scheduler-tick button (needs new backend endpoint), Export Failed
Logs CSV dump.
197 lines
6.1 KiB
Vue
197 lines
6.1 KiB
Vue
<template>
|
|
<div>
|
|
<ExtensionKeyBar class="mb-4" />
|
|
|
|
<v-alert v-if="credentialsStore.error" type="error" variant="tonal" closable class="mb-4">
|
|
{{ String(credentialsStore.error) }}
|
|
</v-alert>
|
|
|
|
<h3 class="text-h6 mb-3">Platform credentials</h3>
|
|
<v-row>
|
|
<v-col
|
|
v-for="p in platformsStore.list"
|
|
:key="p.key"
|
|
cols="12" md="6"
|
|
>
|
|
<CredentialCard
|
|
:platform="p"
|
|
:credential="credentialsStore.byPlatform.get(p.key) || null"
|
|
@replace="openUpload"
|
|
@remove="confirmRemove"
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<h3 class="text-h6 mb-3 mt-6">Downloader</h3>
|
|
<v-card variant="outlined">
|
|
<v-card-text v-if="importStore.settings">
|
|
<v-row>
|
|
<v-col cols="12" sm="6">
|
|
<v-text-field
|
|
v-model.number="dl.download_rate_limit_seconds"
|
|
label="Rate limit (seconds between requests)"
|
|
type="number" step="0.5" min="0"
|
|
density="compact" hide-details
|
|
@blur="saveDownloader"
|
|
/>
|
|
<div class="fc-help">gallery-dl extractor.sleep. Higher = slower but safer.</div>
|
|
</v-col>
|
|
<v-col cols="12" sm="6">
|
|
<v-switch
|
|
v-model="dl.download_validate_files"
|
|
label="Validate downloaded files (magic-byte check)"
|
|
density="compact" hide-details color="accent"
|
|
@change="saveDownloader"
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card-text>
|
|
<v-card-text v-else>
|
|
<v-skeleton-loader type="paragraph" />
|
|
</v-card-text>
|
|
</v-card>
|
|
|
|
<h3 class="text-h6 mb-3 mt-6">Schedule defaults</h3>
|
|
<v-card variant="outlined">
|
|
<v-card-text v-if="importStore.settings">
|
|
<v-row>
|
|
<v-col cols="12" sm="4">
|
|
<v-text-field
|
|
v-model.number="dl.download_schedule_default_seconds"
|
|
label="Default check interval (seconds)"
|
|
type="number" :min="60" :max="86400"
|
|
density="compact" hide-details
|
|
@blur="saveDownloader"
|
|
/>
|
|
<div class="fc-help">
|
|
Used when a source has no per-source or per-artist override.
|
|
Default 28800 (8 hours).
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" sm="4">
|
|
<v-text-field
|
|
v-model.number="dl.download_event_retention_days"
|
|
label="Event retention (days)"
|
|
type="number" :min="1" :max="3650"
|
|
density="compact" hide-details
|
|
@blur="saveDownloader"
|
|
/>
|
|
<div class="fc-help">
|
|
Completed download events older than this are deleted nightly.
|
|
Default 90.
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" sm="4">
|
|
<v-text-field
|
|
v-model.number="dl.download_failure_warning_threshold"
|
|
label="Failure warning threshold"
|
|
type="number" :min="1" :max="100"
|
|
density="compact" hide-details
|
|
@blur="saveDownloader"
|
|
/>
|
|
<div class="fc-help">
|
|
Source row badge turns red after this many consecutive
|
|
failures. Sources are never auto-disabled. Default 5.
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
<v-alert v-if="importStore.settingsError" type="error" variant="tonal" class="mt-2" closable>
|
|
{{ importStore.settingsError }}
|
|
</v-alert>
|
|
</v-card-text>
|
|
<v-card-text v-else>
|
|
<v-skeleton-loader type="paragraph" />
|
|
</v-card-text>
|
|
</v-card>
|
|
|
|
<CredentialUploadDialog
|
|
v-model="showUpload"
|
|
:platform="uploadPlatform"
|
|
@saved="onSaved"
|
|
/>
|
|
|
|
<v-dialog v-model="removeConfirm.open" max-width="420">
|
|
<v-card>
|
|
<v-card-title>Delete {{ removeConfirm.platform?.name }} credential?</v-card-title>
|
|
<v-card-text>
|
|
The encrypted credential will be removed permanently. You'll need to
|
|
re-upload to use this platform again.
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn variant="text" @click="removeConfirm.open = false">Cancel</v-btn>
|
|
<v-btn color="error" variant="flat" @click="doRemove">Delete</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, reactive, ref, watch } from 'vue'
|
|
import { usePlatformsStore } from '../../stores/platforms.js'
|
|
import { useCredentialsStore } from '../../stores/credentials.js'
|
|
import { useImportStore } from '../../stores/import.js'
|
|
import ExtensionKeyBar from '../credentials/ExtensionKeyBar.vue'
|
|
import CredentialUploadDialog from '../credentials/CredentialUploadDialog.vue'
|
|
import CredentialCard from './CredentialCard.vue'
|
|
|
|
const platformsStore = usePlatformsStore()
|
|
const credentialsStore = useCredentialsStore()
|
|
const importStore = useImportStore()
|
|
|
|
const showUpload = ref(false)
|
|
const uploadPlatform = ref(null)
|
|
const removeConfirm = reactive({ open: false, platform: null })
|
|
|
|
const dl = reactive({
|
|
download_rate_limit_seconds: 3.0,
|
|
download_validate_files: true,
|
|
download_schedule_default_seconds: 28800,
|
|
download_event_retention_days: 90,
|
|
download_failure_warning_threshold: 5,
|
|
})
|
|
|
|
watch(() => importStore.settings, (s) => { if (s) Object.assign(dl, s) }, { immediate: true })
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([
|
|
platformsStore.loadAll(),
|
|
credentialsStore.loadAll(),
|
|
importStore.loadSettings(),
|
|
])
|
|
})
|
|
|
|
function openUpload(platform) {
|
|
uploadPlatform.value = platform
|
|
showUpload.value = true
|
|
}
|
|
|
|
async function onSaved() {
|
|
await credentialsStore.loadAll()
|
|
}
|
|
|
|
function confirmRemove(platform) {
|
|
removeConfirm.platform = platform
|
|
removeConfirm.open = true
|
|
}
|
|
|
|
async function doRemove() {
|
|
await credentialsStore.remove(removeConfirm.platform.key)
|
|
removeConfirm.open = false
|
|
await credentialsStore.loadAll()
|
|
}
|
|
|
|
async function saveDownloader() {
|
|
await importStore.patchSettings({ ...dl })
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-help {
|
|
font-size: 12px;
|
|
color: rgb(var(--v-theme-on-surface-variant));
|
|
margin-top: 2px;
|
|
}
|
|
</style>
|