feat: cloud native file browser, settings Claude auth, deploy hardening

- Add native Cloud file browser with FileBrowser API integration
- Add cloud store, filebrowser-client, useAudioPlayer, useFileType composables
- Add Cloud components: FileGrid, FileCard, FileCardGrid, CloudToolbar
- Add Claude authentication section to Settings with OAuth status check
- Harden deploy script to preserve /aiui/ and claude-login.html
- Add nginx proxies for btcpay, homeassistant, filebrowser (HTTPS block)
- Add app configs for filebrowser, searxng, penpot in package.rs
- Update goal progress tracking with app aliases
- Improve mobile back button composable with ResizeObserver
- Update various views with cloud integration and UI refinements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-04 23:05:01 +00:00
parent 173bf8fc0f
commit d7ff678e9d
26 changed files with 2053 additions and 265 deletions

View File

@@ -1,128 +1,185 @@
<template>
<div>
<div class="mb-8">
<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">Cloud services and storage</p>
<p class="text-white/70">Your files, photos, and media</p>
</div>
<button
@click="openNextcloud"
class="glass-button px-6 py-3 rounded-lg font-medium flex items-center gap-2"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
Open Nextcloud
</button>
</div>
</div>
<!-- Folders Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Content Type Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div
v-for="folder in folders"
:key="folder.id"
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="openFolder(folder.id)"
@click="openSection(section)"
>
<div class="flex items-center gap-4 mb-4">
<div class="flex-shrink-0">
<svg class="w-12 h-12 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-for="(path, index) in getFolderIcon(folder.type)"
<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"
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-1 truncate">
{{ folder.name }}
</h3>
<p class="text-sm text-white/60">
{{ folder.itemCount }} {{ folder.itemCount === 1 ? 'item' : 'items' }}
</p>
<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="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 { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAppLauncherStore } from '@/stores/appLauncher'
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>>({})
interface Folder {
id: string
name: string
type: 'pictures' | 'videos' | 'music' | 'documents' | 'downloads'
itemCount: number
const APP_ALIASES: Record<string, string[]> = {
immich: ['immich_server', 'immich-server'],
nextcloud: ['nextcloud-aio', 'nextcloud-server'],
}
const folders = ref<Folder[]>([
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: 'pictures',
name: 'Pictures',
type: 'pictures',
itemCount: 0,
},
{
id: 'videos',
name: 'Videos',
type: 'videos',
itemCount: 0,
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',
type: 'music',
itemCount: 0,
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',
type: 'documents',
itemCount: 0,
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: 'downloads',
name: 'Downloads',
type: 'downloads',
itemCount: 0,
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',
},
])
]
function getFolderIcon(type: string): string[] {
const icons: Record<string, string[]> = {
pictures: ['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'],
videos: ['M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z'],
music: ['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'],
documents: ['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'],
downloads: ['M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4'],
const SECTION_PATHS: Record<string, string> = {
photos: '/Photos',
music: '/Music',
documents: '/Documents',
files: '/',
}
async function loadCounts() {
if (!fileBrowserRunning.value) return
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 {
// Silently fail
}
return icons[type] || ['M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z']
}
function openFolder(folderId: string) {
router.push({
name: 'cloud-folder',
params: { folderId }
})
}
onMounted(() => loadCounts())
function openNextcloud() {
const host = window.location.hostname
const url = `http://${host}:8086`
useAppLauncherStore().open({ url, title: 'Nextcloud' })
function openSection(section: ContentSection) {
router.push({ name: 'cloud-folder', params: { folderId: section.id } })
}
</script>