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
+10 -2
View File
@@ -58,9 +58,14 @@ const route = useRoute()
const notificationStore = useNotificationStore()
const wsStore = useWebSocketStore()
// Initialize WebSocket listeners
// Initialize WebSocket listeners and restore theme
onMounted(() => {
wsStore.init()
// Restore theme preference from localStorage
const savedTheme = localStorage.getItem('theme')
if (savedTheme) {
theme.global.name.value = savedTheme
}
})
const drawer = ref(true)
@@ -70,6 +75,7 @@ const navItems = [
{ title: 'Subscriptions', icon: 'mdi-account-multiple', path: '/subscriptions' },
{ title: 'Downloads', icon: 'mdi-download', path: '/downloads' },
{ title: 'Credentials', icon: 'mdi-key', path: '/credentials' },
{ title: 'Logs', icon: 'mdi-text-box-outline', path: '/logs' },
{ title: 'Settings', icon: 'mdi-cog', path: '/settings' },
]
@@ -81,7 +87,9 @@ const currentPageTitle = computed(() => {
const isDark = computed(() => theme.global.current.value.dark)
const toggleTheme = () => {
theme.global.name.value = isDark.value ? 'light' : 'dark'
const newTheme = isDark.value ? 'light' : 'dark'
theme.global.name.value = newTheme
localStorage.setItem('theme', newTheme)
}
const notification = computed(() => notificationStore)
+5
View File
@@ -31,6 +31,11 @@ const routes = [
name: 'Settings',
component: () => import('../views/Settings.vue'),
},
{
path: '/logs',
name: 'Logs',
component: () => import('../views/Logs.vue'),
},
]
const router = createRouter({
+2
View File
@@ -36,6 +36,7 @@ export const downloadsApi = {
get: (id) => api.get(`/downloads/${id}`),
retry: (id) => api.post(`/downloads/${id}/retry`),
stats: (params = {}) => api.get('/downloads/stats', { params }),
recentActivity: (params = {}) => api.get('/downloads/recent-activity', { params }),
}
// Credentials API
@@ -54,6 +55,7 @@ export const settingsApi = {
updateGalleryDL: (config) => api.put('/settings/gallery-dl', { config }),
getApiKey: () => api.get('/settings/api-key'),
regenerateApiKey: () => api.post('/settings/api-key/regenerate'),
getLogs: (params = {}) => api.get('/settings/logs', { params }),
}
// Platforms API
+33
View File
@@ -0,0 +1,33 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { credentialsApi } from '../services/api'
export const useCredentialsStore = defineStore('credentials', () => {
const credentials = ref([])
const platforms = ref({})
const loading = ref(false)
async function fetchCredentials() {
loading.value = true
try {
const response = await credentialsApi.list()
credentials.value = response.data.items
platforms.value = response.data.platforms || {}
return response.data
} finally {
loading.value = false
}
}
function hasPlatformCredentials(platform) {
return !!platforms.value[platform]
}
return {
credentials,
platforms,
loading,
fetchCredentials,
hasPlatformCredentials,
}
})
+11 -2
View File
@@ -4,6 +4,7 @@ import { downloadsApi } from '../services/api'
export const useDownloadsStore = defineStore('downloads', () => {
const downloads = ref([])
const recentActivity = ref([])
const stats = ref(null)
const loading = ref(false)
const total = ref(0)
@@ -13,9 +14,9 @@ export const useDownloadsStore = defineStore('downloads', () => {
async function fetchDownloads(params = {}) {
loading.value = true
try {
// Use provided per_page or default to 1000 for client-side pagination
const response = await downloadsApi.list({
page: currentPage.value,
per_page: perPage.value,
per_page: params.per_page || 1000,
...params,
})
downloads.value = response.data.items
@@ -26,6 +27,12 @@ export const useDownloadsStore = defineStore('downloads', () => {
}
}
async function fetchRecentActivity(params = {}) {
const response = await downloadsApi.recentActivity(params)
recentActivity.value = response.data.items
return response.data
}
async function fetchStats(params = {}) {
const response = await downloadsApi.stats(params)
stats.value = response.data
@@ -38,12 +45,14 @@ export const useDownloadsStore = defineStore('downloads', () => {
return {
downloads,
recentActivity,
stats,
loading,
total,
currentPage,
perPage,
fetchDownloads,
fetchRecentActivity,
fetchStats,
retryDownload,
}
+2 -2
View File
@@ -15,9 +15,9 @@ export const useSubscriptionsStore = defineStore('subscriptions', () => {
async function fetchSubscriptions(params = {}) {
loading.value = true
try {
// Fetch all items for client-side pagination in v-data-table
const response = await subscriptionsApi.list({
page: currentPage.value,
per_page: perPage.value,
per_page: 1000,
...params,
})
subscriptions.value = response.data.items
+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
+149
View File
@@ -0,0 +1,149 @@
<template>
<div>
<v-row class="mb-4">
<v-col>
<v-btn
color="primary"
prepend-icon="mdi-refresh"
@click="loadLogs"
:loading="loading"
>
Refresh
</v-btn>
</v-col>
<v-col cols="auto">
<v-select
v-model="filterStatus"
:items="statusOptions"
label="Status"
density="compact"
variant="outlined"
clearable
style="min-width: 140px"
/>
</v-col>
</v-row>
<v-card>
<v-card-title>Recent Download Logs</v-card-title>
<v-card-text>
<div v-if="loading" class="text-center py-8">
<v-progress-circular indeterminate />
</div>
<v-expansion-panels v-else-if="filteredLogs.length">
<v-expansion-panel
v-for="log in filteredLogs"
:key="log.id"
>
<v-expansion-panel-title>
<div class="d-flex align-center ga-2" style="width: 100%">
<v-chip
:color="getStatusColor(log.status)"
size="small"
variant="tonal"
>
{{ log.status }}
</v-chip>
<span class="text-truncate" style="max-width: 400px">
{{ log.url }}
</span>
<v-spacer />
<span class="text-caption text-grey">
{{ formatDate(log.created_at) }}
</span>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
<div v-if="log.error_message" class="mb-4">
<div class="text-subtitle-2 text-error">Error</div>
<v-code class="d-block pa-2 bg-error-lighten-5">{{ log.error_message }}</v-code>
</div>
<div v-if="log.stdout" class="mb-4">
<div class="text-subtitle-2">Output</div>
<pre class="log-output bg-grey-lighten-4 pa-2 rounded">{{ log.stdout }}</pre>
</div>
<div v-if="log.stderr">
<div class="text-subtitle-2 text-warning">Errors/Warnings</div>
<pre class="log-output bg-warning-lighten-5 pa-2 rounded">{{ log.stderr }}</pre>
</div>
<div v-if="!log.stdout && !log.stderr && !log.error_message" class="text-grey">
No log output available
</div>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<div v-else class="text-center text-grey py-8">
<v-icon size="48" class="mb-2">mdi-text-box-outline</v-icon>
<div>No logs available</div>
</div>
</v-card-text>
</v-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { settingsApi } from '../services/api'
const loading = ref(false)
const logs = ref([])
const filterStatus = ref(null)
const statusOptions = [
{ title: 'Completed', value: 'completed' },
{ title: 'Failed', value: 'failed' },
{ title: 'Running', value: 'running' },
]
const filteredLogs = computed(() => {
if (!filterStatus.value) return logs.value
return logs.value.filter(l => l.status === filterStatus.value)
})
onMounted(() => {
loadLogs()
})
async function loadLogs() {
loading.value = true
try {
const response = await settingsApi.getLogs({ limit: 100 })
logs.value = response.data.logs
} catch (error) {
console.error('Failed to load logs:', error)
} finally {
loading.value = false
}
}
function getStatusColor(status) {
const colors = {
completed: 'success',
failed: 'error',
running: 'info',
pending: 'warning',
}
return colors[status] || 'grey'
}
function formatDate(dateStr) {
if (!dateStr) return ''
return new Date(dateStr).toLocaleString()
}
</script>
<style scoped>
.log-output {
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow: auto;
}
</style>
+185 -1
View File
@@ -5,6 +5,29 @@
<v-btn color="primary" @click="showAddDialog = true" prepend-icon="mdi-plus">
Add Subscription
</v-btn>
<v-btn
class="ml-2"
variant="outlined"
prepend-icon="mdi-download"
@click="exportSubscriptions"
>
Export
</v-btn>
<v-btn
class="ml-2"
variant="outlined"
prepend-icon="mdi-upload"
@click="triggerImport"
>
Import
</v-btn>
<input
ref="importInput"
type="file"
accept=".json"
style="display: none"
@change="handleImport"
/>
</v-col>
<v-col cols="auto">
<v-text-field
@@ -30,14 +53,32 @@
</v-col>
</v-row>
<!-- Bulk Actions Bar -->
<v-row v-if="selectedIds.length > 0" class="mb-2">
<v-col>
<v-card color="primary" variant="tonal" class="pa-2">
<div class="d-flex align-center ga-2">
<span>{{ selectedIds.length }} selected</span>
<v-btn size="small" variant="text" @click="bulkEnable(true)">Enable All</v-btn>
<v-btn size="small" variant="text" @click="bulkEnable(false)">Disable All</v-btn>
<v-btn size="small" variant="text" color="error" @click="bulkDelete">Delete All</v-btn>
<v-spacer />
<v-btn size="small" variant="text" @click="selectedIds = []">Clear Selection</v-btn>
</div>
</v-card>
</v-col>
</v-row>
<v-card>
<v-data-table
v-model="selectedIds"
:headers="headers"
:items="subscriptionsStore.subscriptions"
:loading="subscriptionsStore.loading"
:items-per-page="20"
v-model:expanded="expandedIds"
item-value="id"
show-select
class="elevation-1"
>
<template v-slot:item.name="{ item }">
@@ -135,6 +176,12 @@
<v-icon start size="small">{{ getPlatformIcon(source.platform) }}</v-icon>
{{ source.platform }}
</v-chip>
<v-tooltip v-if="!credentialsStore.hasPlatformCredentials(source.platform)" location="top">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="warning" size="small" class="ml-1">mdi-alert</v-icon>
</template>
No credentials for {{ source.platform }}. Add them in Settings.
</v-tooltip>
</td>
<td class="text-truncate" style="max-width: 300px">
<a :href="source.url" target="_blank" @click.stop>{{ source.url }}</a>
@@ -148,7 +195,12 @@
@update:model-value="toggleSourceEnabled(source)"
/>
</td>
<td>{{ formatDate(source.last_check) }}</td>
<td>
<div>{{ formatDate(source.last_check) }}</div>
<div v-if="source.enabled && source.last_check" class="text-caption text-grey">
Next: {{ formatNextCheck(source) }}
</div>
</td>
<td>
<v-chip v-if="source.error_count > 0" color="error" size="x-small">
{{ source.error_count }}
@@ -273,15 +325,19 @@
<script setup>
import { ref, watch, onMounted } from 'vue'
import { useSubscriptionsStore } from '../stores/subscriptions'
import { useCredentialsStore } from '../stores/credentials'
import { useNotificationStore } from '../stores/notifications'
const subscriptionsStore = useSubscriptionsStore()
const credentialsStore = useCredentialsStore()
const notifications = useNotificationStore()
const searchQuery = ref('')
const filterEnabled = ref(null)
const expandedIds = ref([])
const checkingIds = ref([])
const selectedIds = ref([])
const importInput = ref(null)
// Subscription dialog
const showAddDialog = ref(false)
@@ -330,6 +386,7 @@ const headers = [
onMounted(() => {
loadSubscriptions()
credentialsStore.fetchCredentials()
})
watch([searchQuery, filterEnabled], () => {
@@ -377,6 +434,23 @@ function formatDate(dateStr) {
return new Date(dateStr).toLocaleString()
}
function formatNextCheck(source) {
if (!source.last_check || !source.check_interval) return 'Unknown'
const lastCheck = new Date(source.last_check)
const nextCheck = new Date(lastCheck.getTime() + source.check_interval * 1000)
const now = new Date()
if (nextCheck <= now) return 'Due now'
const diffMs = nextCheck - now
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
if (diffMins < 60) return `in ${diffMins}m`
if (diffHours < 24) return `in ${diffHours}h`
return nextCheck.toLocaleString()
}
async function toggleEnabled(subscription) {
try {
await subscriptionsStore.updateSubscription(subscription.id, { enabled: !subscription.enabled })
@@ -493,6 +567,116 @@ async function deleteSource() {
notifications.error(`Failed to delete: ${error.message}`)
}
}
// Bulk actions
async function bulkEnable(enabled) {
try {
for (const id of selectedIds.value) {
await subscriptionsStore.updateSubscription(id, { enabled })
}
notifications.success(`${selectedIds.value.length} subscriptions ${enabled ? 'enabled' : 'disabled'}`)
selectedIds.value = []
loadSubscriptions()
} catch (error) {
notifications.error(`Failed to update: ${error.message}`)
}
}
async function bulkDelete() {
if (!confirm(`Delete ${selectedIds.value.length} subscriptions? This cannot be undone.`)) return
try {
for (const id of selectedIds.value) {
await subscriptionsStore.deleteSubscription(id)
}
notifications.success(`${selectedIds.value.length} subscriptions deleted`)
selectedIds.value = []
loadSubscriptions()
} catch (error) {
notifications.error(`Failed to delete: ${error.message}`)
}
}
// Import/Export
function exportSubscriptions() {
const data = {
version: 1,
exported_at: new Date().toISOString(),
subscriptions: subscriptionsStore.subscriptions.map(sub => ({
name: sub.name,
description: sub.description,
priority: sub.priority,
enabled: sub.enabled,
sources: sub.sources?.map(s => ({
platform: s.platform,
url: s.url,
enabled: s.enabled,
check_interval: s.check_interval,
})) || [],
})),
}
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `subscriptions-${new Date().toISOString().split('T')[0]}.json`
a.click()
URL.revokeObjectURL(url)
notifications.success(`Exported ${data.subscriptions.length} subscriptions`)
}
function triggerImport() {
importInput.value.click()
}
async function handleImport(event) {
const file = event.target.files[0]
if (!file) return
try {
const text = await file.text()
const data = JSON.parse(text)
if (!data.subscriptions || !Array.isArray(data.subscriptions)) {
throw new Error('Invalid format: missing subscriptions array')
}
let imported = 0
for (const sub of data.subscriptions) {
try {
// Create subscription
const created = await subscriptionsStore.createSubscription({
name: sub.name,
description: sub.description,
priority: sub.priority || 0,
enabled: sub.enabled !== false,
})
// Add sources
if (sub.sources && created.id) {
for (const source of sub.sources) {
await subscriptionsStore.addSource(created.id, {
platform: source.platform,
url: source.url,
enabled: source.enabled !== false,
check_interval: source.check_interval,
})
}
}
imported++
} catch (e) {
console.error(`Failed to import ${sub.name}:`, e)
}
}
notifications.success(`Imported ${imported} of ${data.subscriptions.length} subscriptions`)
loadSubscriptions()
} catch (error) {
notifications.error(`Import failed: ${error.message}`)
}
// Reset input
event.target.value = ''
}
</script>
<style scoped>