bug fixes from sxsw
This commit is contained in:
@@ -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) }} · {{ 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) {
|
||||
|
||||
Reference in New Issue
Block a user