tuning ui and adding adaptive themeing to extension

This commit is contained in:
Bryan Van Deusen
2026-01-25 18:47:06 -05:00
parent dc6755a0c2
commit 997f31ae27
23 changed files with 978 additions and 114 deletions
+209 -46
View File
@@ -59,45 +59,101 @@
</v-col>
</v-row>
<!-- Quick Actions -->
<v-row class="mt-4">
<!-- Downloads by Platform -->
<v-col cols="12" md="6">
<v-col cols="12">
<v-card>
<v-card-title>Downloads by Platform</v-card-title>
<v-card-text class="d-flex align-center ga-4 flex-wrap">
<v-btn
color="primary"
prepend-icon="mdi-refresh"
:loading="checkingAll"
@click="checkAllSources"
>
Check All Sources
</v-btn>
<v-btn
color="warning"
prepend-icon="mdi-replay"
:loading="retryingAll"
:disabled="!failedCount"
@click="retryAllFailed"
>
Retry All Failed ({{ failedCount }})
</v-btn>
<v-spacer />
<div class="text-body-2 text-grey">
<v-icon size="small" class="mr-1">mdi-harddisk</v-icon>
Storage: {{ storageStats?.total_size_formatted || 'Calculating...' }}
<span v-if="storageStats?.file_count" class="ml-2">
({{ storageStats.file_count.toLocaleString() }} files)
</span>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row class="mt-4">
<!-- Running Downloads (auto-refresh) -->
<v-col cols="12" md="4">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="mr-2" :class="{ 'mdi-spin': runningDownloads.length }">
{{ runningDownloads.length ? 'mdi-loading' : 'mdi-download' }}
</v-icon>
Running
<v-chip v-if="runningDownloads.length" size="small" color="info" class="ml-2">
{{ runningDownloads.length }}
</v-chip>
<v-spacer />
<v-btn
v-if="runningDownloads.length"
icon
size="x-small"
variant="text"
@click="refreshRunning"
>
<v-icon>mdi-refresh</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-list v-if="stats?.by_platform">
<v-list-item
v-for="(count, platform) in stats.by_platform"
:key="platform"
>
<template v-slot:prepend>
<v-icon :color="getPlatformColor(platform)">
{{ getPlatformIcon(platform) }}
</v-icon>
</template>
<v-list-item-title class="text-capitalize">
{{ platform }}
<v-list v-if="runningDownloads.length" density="compact">
<v-list-item v-for="dl in runningDownloads" :key="dl.id">
<v-list-item-title class="text-body-2">
{{ truncate(dl.url, 30) }}
</v-list-item-title>
<template v-slot:append>
<v-chip size="small">{{ count }}</v-chip>
</template>
<v-list-item-subtitle>
<v-progress-linear
indeterminate
color="info"
height="4"
class="mt-1"
/>
</v-list-item-subtitle>
</v-list-item>
</v-list>
<div v-else class="text-center text-grey py-4">
No download data yet
<v-icon size="32" class="mb-2">mdi-check-circle-outline</v-icon>
<div>No active downloads</div>
</div>
</v-card-text>
</v-card>
</v-col>
<!-- Recent Activity -->
<v-col cols="12" md="6">
<v-col cols="12" md="8">
<v-card>
<v-card-title>Recent Downloads</v-card-title>
<v-card-title>
Recent Activity
<v-chip v-if="recentActivity.length" size="small" class="ml-2" variant="tonal">
Last 7 days
</v-chip>
</v-card-title>
<v-card-text>
<v-list v-if="recentDownloads.length">
<v-list v-if="recentActivity.length">
<v-list-item
v-for="download in recentDownloads"
v-for="download in recentActivity"
:key="download.id"
>
<template v-slot:prepend>
@@ -109,7 +165,10 @@
{{ truncate(download.url, 40) }}
</v-list-item-title>
<v-list-item-subtitle>
{{ formatDate(download.created_at) }}
{{ formatRelativeTime(download.created_at) }}
<span v-if="download.file_count > 0" class="text-success ml-2">
{{ download.file_count }} new file{{ download.file_count !== 1 ? 's' : '' }}
</span>
</v-list-item-subtitle>
<template v-slot:append>
<v-chip
@@ -117,13 +176,13 @@
size="small"
variant="tonal"
>
{{ download.status }}
{{ download.status === 'completed' ? 'new content' : download.status }}
</v-chip>
</template>
</v-list-item>
</v-list>
<div v-else class="text-center text-grey py-4">
No recent downloads
No recent activity (failures or new downloads)
</div>
</v-card-text>
<v-card-actions>
@@ -196,47 +255,135 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useSourcesStore } from '../stores/sources'
import { useDownloadsStore } from '../stores/downloads'
import { useNotificationStore } from '../stores/notifications'
import { settingsApi } from '../services/api'
const sourcesStore = useSourcesStore()
const downloadsStore = useDownloadsStore()
const notifications = useNotificationStore()
// State
const checkingAll = ref(false)
const retryingAll = ref(false)
const storageStats = ref(null)
const runningDownloads = ref([])
let refreshInterval = null
// Computed
const stats = computed(() => downloadsStore.stats)
const recentDownloads = computed(() => downloadsStore.downloads.slice(0, 5))
const recentActivity = computed(() => downloadsStore.recentActivity)
const sourcesWithErrors = computed(() =>
sourcesStore.sources.filter(s => s.error_count > 0).slice(0, 5)
)
const failedCount = computed(() => stats.value?.failed || 0)
onMounted(async () => {
await loadDashboardData()
// Start auto-refresh for running downloads
startAutoRefresh()
})
onUnmounted(() => {
stopAutoRefresh()
})
async function loadDashboardData() {
await Promise.all([
sourcesStore.fetchSources(),
downloadsStore.fetchStats(),
downloadsStore.fetchDownloads({ per_page: 5 }),
downloadsStore.fetchRecentActivity({ limit: 10 }),
fetchRunningDownloads(),
fetchStorageStats(),
])
})
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',
async function fetchRunningDownloads() {
try {
const response = await downloadsStore.fetchDownloads({ status: 'running', per_page: 20 })
runningDownloads.value = response.items.filter(d => d.status === 'running')
} catch (e) {
console.error('Failed to fetch running downloads:', e)
}
}
async function fetchStorageStats() {
try {
const response = await settingsApi.get()
storageStats.value = response.data.storage_stats
} catch (e) {
console.error('Failed to fetch storage stats:', e)
}
}
function startAutoRefresh() {
// Refresh running downloads every 5 seconds when there are active downloads
refreshInterval = setInterval(async () => {
if (runningDownloads.value.length > 0 || stats.value?.pending > 0) {
await fetchRunningDownloads()
await downloadsStore.fetchStats()
await downloadsStore.fetchRecentActivity({ limit: 10 })
}
}, 5000)
}
function stopAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval)
refreshInterval = null
}
}
async function refreshRunning() {
await fetchRunningDownloads()
}
async function checkAllSources() {
checkingAll.value = true
try {
const enabledSources = sourcesStore.sources.filter(s => s.enabled)
let queued = 0
for (const source of enabledSources) {
try {
await sourcesStore.triggerCheck(source.id)
queued++
} catch (e) {
console.error(`Failed to queue ${source.id}:`, e)
}
}
notifications.success(`Queued checks for ${queued} sources`)
await fetchRunningDownloads()
} catch (error) {
notifications.error(`Failed to check sources: ${error.message}`)
} finally {
checkingAll.value = false
}
}
async function retryAllFailed() {
retryingAll.value = true
try {
const response = await downloadsStore.fetchDownloads({ status: 'failed', per_page: 100 })
const failed = response.items
let retried = 0
for (const dl of failed) {
try {
await downloadsStore.retryDownload(dl.id)
retried++
} catch (e) {
console.error(`Failed to retry ${dl.id}:`, e)
}
}
notifications.success(`Queued retry for ${retried} downloads`)
await downloadsStore.fetchStats()
await fetchRunningDownloads()
} catch (error) {
notifications.error(`Failed to retry downloads: ${error.message}`)
} finally {
retryingAll.value = false
}
return colors[platform] || 'grey'
}
function getStatusIcon(status) {
@@ -266,6 +413,22 @@ function formatDate(dateStr) {
return new Date(dateStr).toLocaleString()
}
function formatRelativeTime(dateStr) {
if (!dateStr) return 'Unknown'
const date = new Date(dateStr)
const now = new Date()
const diffMs = now - date
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
if (diffMins < 1) return 'Just now'
if (diffMins < 60) return `${diffMins} min${diffMins !== 1 ? 's' : ''} ago`
if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`
if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`
return date.toLocaleDateString()
}
function truncate(str, length) {
if (!str) return ''
if (str.length <= length) return str