bug fixes from sxsw

This commit is contained in:
Dorian
2026-03-14 17:12:41 +00:00
parent dcddc7a5dd
commit b786f68e7a
50 changed files with 1635 additions and 543 deletions

View File

@@ -1,118 +1,92 @@
<template>
<div>
<div class="flex items-center gap-3 mb-6">
<button class="glass-button p-2 rounded-lg" @click="router.push({ name: 'cloud' })">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="pb-6">
<!-- Header with back button -->
<div class="shrink-0 mb-4">
<button @click="goBack" class="hidden md:flex mb-4 items-center gap-2 text-white/70 hover:text-white transition-colors">
<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="M15 19l-7-7 7-7" />
</svg>
Back to Cloud
</button>
<div>
<h1 class="text-2xl font-bold text-white">Peer Files</h1>
<p class="text-sm text-white/50">Browse files shared by federated nodes</p>
</div>
</div>
<!-- Peer list -->
<div v-if="!selectedPeer" class="space-y-3">
<div v-if="loading" class="glass-card p-8 text-center">
<p class="text-white/50 animate-pulse">Loading federation peers...</p>
</div>
<div v-else-if="peers.length === 0" class="glass-card p-8 text-center">
<p class="text-white/50">No federated peers found. Join a federation from Settings to share files.</p>
</div>
<div
v-for="peer in peers"
:key="peer.did"
data-controller-container
tabindex="0"
class="glass-card p-5 cursor-pointer transition-all hover:-translate-y-0.5 hover:bg-white/10"
@click="browsePeer(peer)"
>
<div class="flex items-center gap-4">
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-purple-500/20 flex items-center justify-center">
<svg class="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-base font-semibold text-white truncate">{{ peer.name || truncateDid(peer.did) }}</h3>
<p class="text-xs text-white/40 truncate">{{ peer.onion }}</p>
</div>
<div class="flex items-center gap-2">
<span
class="text-xs px-2 py-0.5 rounded-full"
:class="peer.trust_level === 'trusted' ? 'bg-green-500/15 text-green-400' : 'bg-yellow-500/15 text-yellow-400'"
>
{{ peer.trust_level }}
</span>
<svg class="w-4 h-4 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>
</div>
</div>
</div>
<!-- Peer content catalog -->
<div v-else>
<div class="flex items-center gap-3 mb-4">
<button class="glass-button p-2 rounded-lg" @click="selectedPeer = null">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<!-- Mobile Back Button -->
<Teleport to="body">
<button
@click="goBack"
class="md:hidden mobile-back-btn glass-button px-6 py-3 rounded-lg font-medium shadow-2xl flex items-center justify-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="M15 19l-7-7 7-7" />
</svg>
<span>Back to Cloud</span>
</button>
<div>
<h2 class="text-lg font-semibold text-white">{{ selectedPeer.name || truncateDid(selectedPeer.did) }}</h2>
<p class="text-xs text-white/40">{{ selectedPeer.onion }}</p>
</Teleport>
<!-- Peer Header -->
<div class="flex items-center gap-4">
<div class="flex-shrink-0 w-12 h-12 rounded-xl flex items-center justify-center bg-purple-500/15">
<svg class="w-7 h-7 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
</svg>
</div>
<div class="hidden md:block">
<h1 class="text-2xl font-bold text-white">{{ peerDisplayName }}</h1>
<p class="text-sm text-white/50">{{ currentPeer?.onion || 'Peer files' }}</p>
</div>
</div>
</div>
<div v-if="catalogLoading" class="glass-card p-8 text-center">
<p class="text-white/50 animate-pulse">Connecting via Tor... This may take a few seconds.</p>
</div>
<!-- Loading -->
<div v-if="loading" class="glass-card p-8 text-center">
<svg class="animate-spin h-6 w-6 text-purple-400 mx-auto mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p class="text-white/50 text-sm">Connecting via Tor... This may take a few seconds.</p>
</div>
<div v-else-if="catalogError" class="glass-card p-6">
<p class="text-red-400 text-sm">{{ catalogError }}</p>
<button class="glass-button mt-3 px-4 py-2 rounded-lg text-sm" @click="browsePeer(selectedPeer!)">Retry</button>
</div>
<!-- Error -->
<div v-else-if="catalogError" class="glass-card p-6">
<p class="text-red-400 text-sm mb-3">{{ catalogError }}</p>
<button class="glass-button px-4 py-2 rounded-lg text-sm" @click="loadCatalog">Retry</button>
</div>
<div v-else-if="catalogItems.length === 0" class="glass-card p-8 text-center">
<p class="text-white/50">This peer has no shared files.</p>
</div>
<!-- Empty -->
<div v-else-if="catalogItems.length === 0 && !loading" class="glass-card p-8 text-center">
<p class="text-white/50">This peer has no shared files.</p>
</div>
<div v-else class="space-y-2">
<div
v-for="item in catalogItems"
:key="item.id"
class="glass-card p-4 flex items-center gap-4"
>
<div class="flex-shrink-0 w-9 h-9 rounded-lg flex items-center justify-center" :class="fileIconBg(item.mime_type)">
<svg class="w-5 h-5" :class="fileIconColor(item.mime_type)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="fileIconPath(item.mime_type)" />
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-white truncate">{{ item.filename }}</p>
<p class="text-xs text-white/40">{{ formatSize(item.size_bytes) }} &middot; {{ item.mime_type }}</p>
</div>
<div class="flex items-center gap-2">
<span
class="text-xs px-2 py-0.5 rounded-full"
:class="accessBadgeClass(item.access)"
>
{{ accessLabel(item.access) }}
</span>
<button
v-if="canDownload(item.access)"
class="glass-button px-3 py-1.5 rounded-lg text-xs font-medium"
:disabled="downloading === item.id"
@click="downloadFile(item)"
>
{{ downloading === item.id ? 'Downloading...' : 'Download' }}
</button>
</div>
<!-- File Grid -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div
v-for="item in catalogItems"
:key="item.id"
class="glass-card p-4 flex items-center gap-4"
>
<div class="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center" :class="fileIconBg(item.mime_type)">
<svg class="w-5 h-5" :class="fileIconColor(item.mime_type)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="fileIconPath(item.mime_type)" />
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-white truncate">{{ item.filename }}</p>
<p class="text-xs text-white/40">{{ formatSize(item.size_bytes) }}</p>
</div>
<div class="flex items-center gap-2">
<span
class="text-xs px-2 py-0.5 rounded-full"
:class="accessBadgeClass(item.access)"
>
{{ accessLabel(item.access) }}
</span>
<button
v-if="canDownload(item.access)"
class="glass-button px-3 py-1.5 rounded-lg text-xs font-medium"
:disabled="downloading === item.id"
@click="downloadFile(item)"
>
{{ downloading === item.id ? '...' : 'Download' }}
</button>
</div>
</div>
</div>
@@ -120,10 +94,14 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted, Teleport } from 'vue'
import { useRouter } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
const props = defineProps<{
peerId?: string
}>()
const router = useRouter()
interface PeerNode {
@@ -144,42 +122,65 @@ interface CatalogItem {
}
const loading = ref(true)
const peers = ref<PeerNode[]>([])
const selectedPeer = ref<PeerNode | null>(null)
const catalogLoading = ref(false)
const currentPeer = ref<PeerNode | null>(null)
const catalogError = ref('')
const catalogItems = ref<CatalogItem[]>([])
const downloading = ref<string | null>(null)
const peerDisplayName = computed(() => {
if (currentPeer.value?.name) return currentPeer.value.name
if (currentPeer.value?.did) return truncateDid(currentPeer.value.did)
return props.peerId ? truncateOnion(props.peerId) : 'Peer Files'
})
function goBack() {
router.push({ name: 'cloud' })
}
onMounted(async () => {
try {
const result = await rpcClient.federationListNodes()
peers.value = result?.nodes ?? []
} catch {
peers.value = []
} finally {
if (props.peerId) {
// Find the peer by onion address
try {
const result = await rpcClient.federationListNodes()
const peers = result?.nodes ?? []
currentPeer.value = peers.find((p: PeerNode) => p.onion === props.peerId) || null
} catch {
// Continue with just the onion address
}
await loadCatalog()
} else {
loading.value = false
}
})
async function loadCatalog() {
const onion = props.peerId || currentPeer.value?.onion
if (!onion) return
loading.value = true
catalogError.value = ''
catalogItems.value = []
try {
const result = await rpcClient.call<{ items?: CatalogItem[] }>({
method: 'content.browse-peer',
params: { onion },
timeout: 30000,
})
catalogItems.value = result?.items ?? []
} catch (e: unknown) {
catalogError.value = e instanceof Error ? e.message : 'Failed to connect to peer'
} finally {
loading.value = false
}
}
function truncateDid(did: string): string {
if (did.length <= 24) return did
return did.slice(0, 16) + '...' + did.slice(-8)
}
async function browsePeer(peer: PeerNode) {
selectedPeer.value = peer
catalogLoading.value = true
catalogError.value = ''
catalogItems.value = []
try {
const result = await rpcClient.call<{ items?: CatalogItem[] }>({ method: 'content.browse-peer', params: { onion: peer.onion }, timeout: 30000 })
catalogItems.value = result?.items ?? []
} catch (e: unknown) {
catalogError.value = e instanceof Error ? e.message : 'Failed to connect to peer'
} finally {
catalogLoading.value = false
}
function truncateOnion(onion: string): string {
if (onion.length <= 20) return onion
return onion.slice(0, 12) + '...'
}
function formatSize(bytes: number): string {
@@ -231,12 +232,13 @@ function canDownload(access: CatalogItem['access']): boolean {
}
async function downloadFile(item: CatalogItem) {
if (!selectedPeer.value) return
const onion = props.peerId || currentPeer.value?.onion
if (!onion) return
downloading.value = item.id
try {
const result = await rpcClient.call<{ data?: string }>({
method: 'content.download-peer',
params: { onion: selectedPeer.value.onion, content_id: item.id },
params: { onion, content_id: item.id },
timeout: 120000,
})
if (result?.data) {