tuning ui and adding adaptive themeing to extension
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user