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>
191 lines
6.4 KiB
Vue
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>
|