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:
co-authored by
Claude Opus 4.6
parent
173bf8fc0f
commit
d7ff678e9d
@@ -0,0 +1,121 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { fileBrowserClient, type FileBrowserItem } from '@/api/filebrowser-client'
|
||||
|
||||
export const useCloudStore = defineStore('cloud', () => {
|
||||
const currentPath = ref('/')
|
||||
const items = ref<FileBrowserItem[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const authenticated = ref(false)
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
const parts = currentPath.value.split('/').filter(Boolean)
|
||||
const crumbs = [{ name: 'Home', path: '/' }]
|
||||
let path = ''
|
||||
for (const part of parts) {
|
||||
path += `/${part}`
|
||||
crumbs.push({ name: part, path })
|
||||
}
|
||||
return crumbs
|
||||
})
|
||||
|
||||
const sortedItems = computed(() => {
|
||||
const dirs = items.value.filter((i) => i.isDir)
|
||||
const files = items.value.filter((i) => !i.isDir)
|
||||
dirs.sort((a, b) => a.name.localeCompare(b.name))
|
||||
files.sort((a, b) => a.name.localeCompare(b.name))
|
||||
return [...dirs, ...files]
|
||||
})
|
||||
|
||||
async function init(): Promise<boolean> {
|
||||
if (authenticated.value) return true
|
||||
const ok = await fileBrowserClient.login()
|
||||
authenticated.value = ok
|
||||
return ok
|
||||
}
|
||||
|
||||
async function navigate(path: string): Promise<void> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
if (!authenticated.value) {
|
||||
const ok = await init()
|
||||
if (!ok) {
|
||||
error.value = 'Failed to authenticate with File Browser'
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await fileBrowserClient.listDirectory(path)
|
||||
items.value = result
|
||||
currentPath.value = path
|
||||
} catch {
|
||||
// Directory may not exist — try to create it, then retry
|
||||
if (path !== '/') {
|
||||
try {
|
||||
const parentPath = path.substring(0, path.lastIndexOf('/')) || '/'
|
||||
const dirName = path.substring(path.lastIndexOf('/') + 1)
|
||||
await fileBrowserClient.createFolder(parentPath, dirName)
|
||||
const result = await fileBrowserClient.listDirectory(path)
|
||||
items.value = result
|
||||
currentPath.value = path
|
||||
} catch {
|
||||
// Fall back to root
|
||||
const result = await fileBrowserClient.listDirectory('/')
|
||||
items.value = result
|
||||
currentPath.value = '/'
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to list root directory')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load files'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
await navigate(currentPath.value)
|
||||
}
|
||||
|
||||
async function uploadFile(file: File): Promise<void> {
|
||||
await fileBrowserClient.upload(currentPath.value, file)
|
||||
await refresh()
|
||||
}
|
||||
|
||||
async function deleteItem(path: string): Promise<void> {
|
||||
await fileBrowserClient.deleteItem(path)
|
||||
await refresh()
|
||||
}
|
||||
|
||||
function downloadUrl(path: string): string {
|
||||
return fileBrowserClient.downloadUrl(path)
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
currentPath.value = '/'
|
||||
items.value = []
|
||||
loading.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
currentPath,
|
||||
items,
|
||||
loading,
|
||||
error,
|
||||
authenticated,
|
||||
breadcrumbs,
|
||||
sortedItems,
|
||||
init,
|
||||
navigate,
|
||||
refresh,
|
||||
uploadFile,
|
||||
deleteItem,
|
||||
downloadUrl,
|
||||
reset,
|
||||
}
|
||||
})
|
||||
@@ -6,6 +6,19 @@ import { useAppStore } from './app'
|
||||
|
||||
const STORAGE_KEY = 'archipelago-goal-progress'
|
||||
|
||||
/** App ID aliases — goal definitions use canonical IDs but the backend may register under variant names */
|
||||
const APP_ALIASES: Record<string, string[]> = {
|
||||
immich: ['immich-server', 'immich-app', 'immich_server'],
|
||||
nextcloud: ['nextcloud-aio', 'nextcloud-server'],
|
||||
'bitcoin-knots': ['bitcoin', 'bitcoin-core'],
|
||||
}
|
||||
|
||||
function matchesAppId(pkgId: string, appId: string): boolean {
|
||||
if (pkgId === appId) return true
|
||||
const aliases = APP_ALIASES[appId]
|
||||
return aliases ? aliases.includes(pkgId) : false
|
||||
}
|
||||
|
||||
export const useGoalStore = defineStore('goals', () => {
|
||||
const progress = ref<Record<string, GoalProgress>>({})
|
||||
|
||||
@@ -34,15 +47,41 @@ export const useGoalStore = defineStore('goals', () => {
|
||||
const appStore = useAppStore()
|
||||
const packages = appStore.packages
|
||||
|
||||
// Auto-sync install step completion from actual package state
|
||||
// This ensures steps tick when apps are installed outside the wizard
|
||||
let didSync = false
|
||||
for (const step of goal.steps) {
|
||||
if (step.appId && step.action === 'install') {
|
||||
const isInstalled = Object.keys(packages).some((pkgId) => matchesAppId(pkgId, step.appId!))
|
||||
if (isInstalled) {
|
||||
if (!progress.value[goalId]) {
|
||||
progress.value[goalId] = {
|
||||
goalId,
|
||||
status: 'in-progress',
|
||||
currentStepIndex: 0,
|
||||
completedSteps: [],
|
||||
startedAt: Date.now(),
|
||||
}
|
||||
didSync = true
|
||||
}
|
||||
if (!progress.value[goalId].completedSteps.includes(step.id)) {
|
||||
progress.value[goalId].completedSteps.push(step.id)
|
||||
didSync = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (didSync) save()
|
||||
|
||||
const allRunning = goal.requiredApps.every((appId) =>
|
||||
Object.entries(packages).some(
|
||||
([pkgId, pkg]) => pkgId === appId && pkg.state === 'running',
|
||||
([pkgId, pkg]) => matchesAppId(pkgId, appId) && pkg.state === 'running',
|
||||
),
|
||||
)
|
||||
if (allRunning) return 'completed'
|
||||
|
||||
const anyInstalled = goal.requiredApps.some((appId) =>
|
||||
Object.keys(packages).some((pkgId) => pkgId === appId),
|
||||
Object.keys(packages).some((pkgId) => matchesAppId(pkgId, appId)),
|
||||
)
|
||||
if (anyInstalled || progress.value[goalId]) return 'in-progress'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user