archy/neode-ui/src/views/Cloud.vue
Dorian 1a07862559 fix: add loading state to Cloud file counts
All 30 UX audit findings verified resolved. Added the last missing
loading indicator for Cloud.vue file count fetching. All P0, P1, and P2
items from docs/ux-audit-2026-03.md are now addressed across Login,
Home, Apps, Server, Chat, Federation, Credentials, Settings, Marketplace,
Web5, SystemUpdate, and Cloud views.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:40:26 +00:00

191 lines
6.4 KiB
Vue

<template>
<div>
<div class="hidden md:block mb-8">
<div class="flex items-center justify-between mb-2">
<div>
<h1 class="text-3xl font-bold text-white mb-2">Cloud</h1>
<p class="text-white/70">Your files, photos, and media</p>
</div>
</div>
</div>
<!-- Content Type Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div
v-for="section in contentSections"
:key="section.id"
data-controller-container
tabindex="0"
class="glass-card p-6 cursor-pointer transition-all hover:-translate-y-1 hover:bg-white/10"
@click="openSection(section)"
>
<div class="flex items-center gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-xl flex items-center justify-center" :class="section.iconBg">
<svg class="w-7 h-7" :class="section.iconColor" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-for="(path, index) in section.iconPaths"
:key="index"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
:d="path"
/>
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-white mb-0.5 truncate">{{ section.name }}</h3>
<p class="text-xs text-white/50">{{ section.description }}</p>
</div>
<!-- Arrow indicator -->
<svg class="w-5 h-5 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
<!-- App status -->
<div class="flex items-center gap-2 text-xs">
<span
class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full"
:class="isAppRunning(section.appId) ? 'bg-green-500/15 text-green-400' : 'bg-white/5 text-white/40'"
>
<span class="w-1.5 h-1.5 rounded-full" :class="isAppRunning(section.appId) ? 'bg-green-400' : 'bg-white/30'"></span>
{{ section.appLabel }}
</span>
<span v-if="!isAppRunning(section.appId)" class="text-white/30">Not installed</span>
<span v-else-if="countsLoading" class="text-white/30 animate-pulse">Loading...</span>
<span v-else-if="sectionCounts[section.id] !== undefined" class="text-white/30">{{ sectionCounts[section.id] }} items</span>
</div>
</div>
</div>
<!-- Not Installed Hint -->
<div v-if="!fileBrowserRunning" class="glass-card p-8 mt-6 text-center">
<p class="text-white/60 mb-3">Install File Browser from the App Store to get started with your cloud storage.</p>
<RouterLink to="/dashboard/marketplace" class="glass-button inline-flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium">
Open App Store
</RouterLink>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { useRouter, RouterLink } from 'vue-router'
import { useAppStore } from '../stores/app'
import { fileBrowserClient } from '@/api/filebrowser-client'
const router = useRouter()
const store = useAppStore()
const sectionCounts = ref<Record<string, number>>({})
const countsLoading = ref(false)
const APP_ALIASES: Record<string, string[]> = {
immich: ['immich_server', 'immich-server'],
nextcloud: ['nextcloud-aio', 'nextcloud-server'],
}
function isAppRunning(appId: string): boolean {
const packages = store.packages
if (packages[appId]?.state === 'running') return true
const aliases = APP_ALIASES[appId]
if (aliases) {
for (const alias of aliases) {
if (packages[alias]?.state === 'running') return true
}
}
return false
}
const fileBrowserRunning = computed(() => isAppRunning('filebrowser'))
interface ContentSection {
id: string
name: string
description: string
appId: string
appLabel: string
iconPaths: string[]
iconBg: string
iconColor: string
}
const contentSections: ContentSection[] = [
{
id: 'photos',
name: 'Photos & Videos',
description: 'Auto-backup & browse your media',
appId: 'filebrowser',
appLabel: 'File Browser',
iconPaths: ['M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'],
iconBg: 'bg-blue-500/15',
iconColor: 'text-blue-400',
},
{
id: 'music',
name: 'Music',
description: 'Your music collection',
appId: 'filebrowser',
appLabel: 'File Browser',
iconPaths: ['M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3'],
iconBg: 'bg-orange-500/15',
iconColor: 'text-orange-400',
},
{
id: 'documents',
name: 'Documents',
description: 'Files, docs & spreadsheets',
appId: 'filebrowser',
appLabel: 'File Browser',
iconPaths: ['M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'],
iconBg: 'bg-green-500/15',
iconColor: 'text-green-400',
},
{
id: 'files',
name: 'All Files',
description: 'Browse your server file system',
appId: 'filebrowser',
appLabel: 'File Browser',
iconPaths: ['M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z'],
iconBg: 'bg-white/10',
iconColor: 'text-white/70',
},
]
const SECTION_PATHS: Record<string, string> = {
photos: '/Photos',
music: '/Music',
documents: '/Documents',
files: '/',
}
async function loadCounts() {
if (!fileBrowserRunning.value) return
countsLoading.value = true
try {
const ok = await fileBrowserClient.login()
if (!ok) return
for (const section of contentSections) {
const path = SECTION_PATHS[section.id]
if (!path) continue
try {
const items = await fileBrowserClient.listDirectory(path)
sectionCounts.value[section.id] = items.length
} catch {
sectionCounts.value[section.id] = 0
}
}
} catch (e) {
if (import.meta.env.DEV) console.warn('FileBrowser count loading failed silently', e)
} finally {
countsLoading.value = false
}
}
onMounted(() => loadCounts())
function openSection(section: ContentSection) {
router.push({ name: 'cloud-folder', params: { folderId: section.id } })
}
</script>