363 lines
12 KiB
Vue
363 lines
12 KiB
Vue
<template>
|
|
<div>
|
|
<v-row>
|
|
<!-- Platform Cards -->
|
|
<v-col
|
|
v-for="(info, platform) in platforms"
|
|
:key="platform"
|
|
cols="12"
|
|
md="6"
|
|
>
|
|
<v-card
|
|
:border="!!getCredential(platform)"
|
|
:style="!getCredential(platform)
|
|
? { border: '2px dashed var(--v-border-color)' }
|
|
: { borderColor: 'rgb(var(--v-theme-success))' }"
|
|
>
|
|
<v-card-title class="d-flex align-center">
|
|
<v-icon :color="getPlatformColor(platform)" class="mr-2">
|
|
{{ getPlatformIcon(platform) }}
|
|
</v-icon>
|
|
{{ info.name }}
|
|
<v-spacer />
|
|
<v-chip
|
|
:color="getCredentialStatus(platform).color"
|
|
size="small"
|
|
>
|
|
{{ getCredentialStatus(platform).text }}
|
|
</v-chip>
|
|
</v-card-title>
|
|
|
|
<v-card-text>
|
|
<p class="text-body-2 mb-4">{{ info.description }}</p>
|
|
|
|
<div v-if="getCredential(platform)" class="mb-4">
|
|
<v-list-item density="compact" class="px-0">
|
|
<template v-slot:prepend>
|
|
<v-icon color="success">mdi-check-circle</v-icon>
|
|
</template>
|
|
<v-list-item-title>Credentials stored</v-list-item-title>
|
|
<v-list-item-subtitle>
|
|
Last verified: {{ formatDate(getCredential(platform).last_verified) }}
|
|
</v-list-item-subtitle>
|
|
</v-list-item>
|
|
|
|
<v-list-item
|
|
v-if="getCredential(platform).expires_at"
|
|
density="compact"
|
|
class="px-0"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="isExpiringSoon(platform) ? 'warning' : 'grey'">
|
|
mdi-clock-outline
|
|
</v-icon>
|
|
</template>
|
|
<v-list-item-title>
|
|
Expires: {{ formatDate(getCredential(platform).expires_at) }}
|
|
</v-list-item-title>
|
|
</v-list-item>
|
|
</div>
|
|
|
|
<div v-else class="text-center py-4 mb-4">
|
|
<v-icon size="40" color="secondary" :style="{ opacity: 0.5 }">mdi-key-remove</v-icon>
|
|
<div class="text-body-2 mt-2">No credentials configured</div>
|
|
<div class="text-caption text-medium-emphasis mt-1">Add credentials to enable downloads</div>
|
|
</div>
|
|
</v-card-text>
|
|
|
|
<v-card-actions>
|
|
<v-btn
|
|
v-if="getCredential(platform)"
|
|
variant="text"
|
|
color="primary"
|
|
@click="verifyCredential(platform)"
|
|
:loading="verifyingPlatform === platform"
|
|
>
|
|
Verify
|
|
</v-btn>
|
|
<v-btn
|
|
:variant="getCredential(platform) ? 'text' : 'flat'"
|
|
color="primary"
|
|
@click="openUploadDialog(platform, info)"
|
|
>
|
|
{{ getCredential(platform) ? 'Update' : 'Add' }} Credentials
|
|
</v-btn>
|
|
<v-spacer />
|
|
<v-btn
|
|
v-if="getCredential(platform)"
|
|
variant="text"
|
|
color="error"
|
|
@click="confirmDelete(platform)"
|
|
>
|
|
Remove
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Upload Dialog -->
|
|
<v-dialog v-model="uploadDialog" max-width="600">
|
|
<v-card>
|
|
<v-card-title>
|
|
{{ uploadPlatform?.name }} Credentials
|
|
</v-card-title>
|
|
|
|
<v-card-text>
|
|
<v-alert type="info" variant="tonal" class="mb-4">
|
|
<template v-if="uploadPlatformKey === 'pixiv'">
|
|
Paste your Pixiv OAuth refresh token below.
|
|
Run <code>gallery-dl oauth:pixiv</code> to obtain one.
|
|
</template>
|
|
<template v-else-if="uploadPlatform?.auth_type === 'token'">
|
|
Paste your Discord user token below.
|
|
<strong>Never share your token with anyone!</strong>
|
|
</template>
|
|
<template v-else>
|
|
Paste your cookies in Netscape format, or use the Firefox extension
|
|
for automatic export.
|
|
</template>
|
|
</v-alert>
|
|
|
|
<v-textarea
|
|
v-model="credentialData"
|
|
:label="uploadPlatform?.auth_type === 'token' ? 'Token' : 'Cookies (Netscape format)'"
|
|
rows="8"
|
|
variant="outlined"
|
|
:placeholder="getPlaceholder()"
|
|
/>
|
|
|
|
<v-expansion-panels class="mt-4">
|
|
<v-expansion-panel>
|
|
<v-expansion-panel-title>
|
|
How to get {{ uploadPlatform?.auth_type === 'token' ? 'your token' : 'cookies' }}
|
|
</v-expansion-panel-title>
|
|
<v-expansion-panel-text>
|
|
<template v-if="uploadPlatformKey === 'pixiv'">
|
|
<ol>
|
|
<li>Install gallery-dl: <code>pip install gallery-dl</code></li>
|
|
<li>Run: <code>gallery-dl oauth:pixiv</code></li>
|
|
<li>A browser window will open for Pixiv login</li>
|
|
<li>After authorizing, copy the refresh token displayed</li>
|
|
<li>Paste the token here</li>
|
|
</ol>
|
|
<p class="mt-2 text-caption">The refresh token is a long string that gallery-dl uses to authenticate with Pixiv's API.</p>
|
|
</template>
|
|
<template v-else-if="uploadPlatform?.auth_type === 'token'">
|
|
<ol>
|
|
<li>Open Discord in your browser</li>
|
|
<li>Press F12 to open Developer Tools</li>
|
|
<li>Go to the Network tab</li>
|
|
<li>Refresh the page</li>
|
|
<li>Look for a request to discord.com/api</li>
|
|
<li>Find the "Authorization" header in the request headers</li>
|
|
<li>Copy the token value (without "Bearer " prefix)</li>
|
|
</ol>
|
|
</template>
|
|
<template v-else>
|
|
<p><strong>Option 1: Firefox Extension (Recommended)</strong></p>
|
|
<p>Install our Firefox extension to automatically export cookies.</p>
|
|
<p class="mt-4"><strong>Option 2: Browser Export</strong></p>
|
|
<ol>
|
|
<li>Install a "cookies.txt" browser extension</li>
|
|
<li>Visit {{ uploadPlatform?.name }} and log in</li>
|
|
<li>Use the extension to export cookies</li>
|
|
<li>Paste the exported text here</li>
|
|
</ol>
|
|
</template>
|
|
</v-expansion-panel-text>
|
|
</v-expansion-panel>
|
|
</v-expansion-panels>
|
|
</v-card-text>
|
|
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn variant="text" @click="uploadDialog = false">Cancel</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
:loading="uploading"
|
|
:disabled="!credentialData"
|
|
@click="uploadCredentials"
|
|
>
|
|
Save
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<!-- Delete Confirmation -->
|
|
<v-dialog v-model="deleteDialog" max-width="400">
|
|
<v-card>
|
|
<v-card-title>Remove Credentials</v-card-title>
|
|
<v-card-text>
|
|
Are you sure you want to remove credentials for
|
|
<strong>{{ platforms[deletePlatform]?.name }}</strong>?
|
|
Downloads from this platform may fail without valid credentials.
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn variant="text" @click="deleteDialog = false">Cancel</v-btn>
|
|
<v-btn color="error" @click="deleteCredential">Remove</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { credentialsApi, platformsApi } from '../services/api'
|
|
import { useNotificationStore } from '../stores/notifications'
|
|
|
|
const notifications = useNotificationStore()
|
|
|
|
const platforms = ref({})
|
|
const credentials = ref([])
|
|
const loading = ref(false)
|
|
const uploadDialog = ref(false)
|
|
const uploadPlatform = ref(null)
|
|
const uploadPlatformKey = ref('')
|
|
const credentialData = ref('')
|
|
const uploading = ref(false)
|
|
const deleteDialog = ref(false)
|
|
const deletePlatform = ref('')
|
|
const verifyingPlatform = ref('')
|
|
|
|
onMounted(() => {
|
|
// Don't block UI - load data in background
|
|
loadData()
|
|
})
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const [platformsRes, credentialsRes] = await Promise.all([
|
|
platformsApi.list(),
|
|
credentialsApi.list(),
|
|
])
|
|
platforms.value = platformsRes.data.platforms
|
|
credentials.value = credentialsRes.data.items
|
|
} catch (error) {
|
|
notifications.error(`Failed to load: ${error.message}`)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function getPlatformIcon(platform) {
|
|
const icons = {
|
|
patreon: 'mdi-patreon',
|
|
subscribestar: 'mdi-star',
|
|
hentaifoundry: 'mdi-palette',
|
|
discord: 'mdi-discord',
|
|
pixiv: 'mdi-alpha-p-box',
|
|
deviantart: 'mdi-deviantart',
|
|
}
|
|
return icons[platform] || 'mdi-web'
|
|
}
|
|
|
|
function getPlatformColor(platform) {
|
|
const colors = {
|
|
patreon: '#FF424D',
|
|
subscribestar: '#FFD700',
|
|
hentaifoundry: '#9C27B0',
|
|
discord: '#5865F2',
|
|
pixiv: '#0096FA',
|
|
deviantart: '#05CC47',
|
|
}
|
|
return colors[platform] || 'grey'
|
|
}
|
|
|
|
function getCredential(platform) {
|
|
return credentials.value.find(c => c.platform === platform)
|
|
}
|
|
|
|
function getCredentialStatus(platform) {
|
|
const cred = getCredential(platform)
|
|
if (!cred) return { color: 'grey', text: 'Not configured' }
|
|
if (isExpiringSoon(platform)) return { color: 'warning', text: 'Expiring soon' }
|
|
return { color: 'success', text: 'Active' }
|
|
}
|
|
|
|
function isExpiringSoon(platform) {
|
|
const cred = getCredential(platform)
|
|
if (!cred?.expires_at) return false
|
|
const expiresAt = new Date(cred.expires_at)
|
|
const daysUntilExpiry = (expiresAt - new Date()) / (1000 * 60 * 60 * 24)
|
|
return daysUntilExpiry < 7
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return 'Never'
|
|
return new Date(dateStr).toLocaleString()
|
|
}
|
|
|
|
function openUploadDialog(platform, info) {
|
|
uploadPlatformKey.value = platform
|
|
uploadPlatform.value = info
|
|
credentialData.value = ''
|
|
uploadDialog.value = true
|
|
}
|
|
|
|
function getPlaceholder() {
|
|
if (uploadPlatformKey.value === 'pixiv') {
|
|
return 'AZX1abc2DEF3ghi...'
|
|
}
|
|
if (uploadPlatform.value?.auth_type === 'token') {
|
|
return 'mfa.AbCdEf123456...'
|
|
}
|
|
return '# Netscape HTTP Cookie File\n.patreon.com\tTRUE\t/\tTRUE\t0\tsession_id\tabc123...'
|
|
}
|
|
|
|
async function uploadCredentials() {
|
|
uploading.value = true
|
|
try {
|
|
await credentialsApi.upload({
|
|
platform: uploadPlatformKey.value,
|
|
credential_type: uploadPlatform.value?.auth_type === 'token' ? 'token' : 'cookies',
|
|
data: credentialData.value,
|
|
})
|
|
notifications.success('Credentials saved')
|
|
uploadDialog.value = false
|
|
await loadData()
|
|
} catch (error) {
|
|
notifications.error(`Failed to save: ${error.response?.data?.error || error.message}`)
|
|
} finally {
|
|
uploading.value = false
|
|
}
|
|
}
|
|
|
|
async function verifyCredential(platform) {
|
|
verifyingPlatform.value = platform
|
|
try {
|
|
const response = await credentialsApi.verify(platform)
|
|
if (response.data.is_valid) {
|
|
notifications.success(`${platforms.value[platform].name} credentials verified`)
|
|
} else {
|
|
notifications.warning(`Verification failed: ${response.data.error || 'Unknown error'}`)
|
|
}
|
|
await loadData()
|
|
} catch (error) {
|
|
notifications.error(`Verification failed: ${error.message}`)
|
|
} finally {
|
|
verifyingPlatform.value = ''
|
|
}
|
|
}
|
|
|
|
function confirmDelete(platform) {
|
|
deletePlatform.value = platform
|
|
deleteDialog.value = true
|
|
}
|
|
|
|
async function deleteCredential() {
|
|
try {
|
|
await credentialsApi.delete(deletePlatform.value)
|
|
notifications.success('Credentials removed')
|
|
deleteDialog.value = false
|
|
await loadData()
|
|
} catch (error) {
|
|
notifications.error(`Failed to remove: ${error.message}`)
|
|
}
|
|
}
|
|
</script>
|