bug fixes from sxsw
This commit is contained in:
@@ -417,7 +417,7 @@
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
@click="closeUninstallModal()"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div class="absolute inset-0 bg-black/10 backdrop-blur-md"></div>
|
||||
<div
|
||||
ref="uninstallModalRef"
|
||||
@click.stop
|
||||
@@ -486,7 +486,6 @@ const appId = computed(() => route.params.id as string)
|
||||
|
||||
// Web-only app detection (no container — external websites)
|
||||
const WEB_ONLY_APP_URLS: Record<string, string> = {
|
||||
'indeedhub': 'https://archipelago.indeehub.studio',
|
||||
'botfights': 'https://botfights.net',
|
||||
'nwnn': 'https://nwnn.l484.com',
|
||||
'484-kitchen': 'https://484.kitchen',
|
||||
@@ -528,6 +527,7 @@ const ROUTE_TO_PACKAGE_KEY: Record<string, string> = {
|
||||
portainer: 'portainer',
|
||||
'uptime-kuma': 'uptime-kuma',
|
||||
tailscale: 'tailscale',
|
||||
indeedhub: 'indeedhub',
|
||||
}
|
||||
|
||||
/** Backend may register under variant container names */
|
||||
@@ -740,8 +740,8 @@ function launchApp() {
|
||||
prod: 'http://localhost:8103' // Self-hosted splash screen
|
||||
},
|
||||
'indeedhub': {
|
||||
dev: 'https://archipelago.indeehub.studio',
|
||||
prod: 'https://archipelago.indeehub.studio'
|
||||
dev: 'http://localhost:8190',
|
||||
prod: 'http://localhost:8190'
|
||||
},
|
||||
// Dummy apps - replace with real URLs when packaged
|
||||
'bitcoin': {
|
||||
|
||||
+15
-17
@@ -1,24 +1,28 @@
|
||||
<template>
|
||||
<div class="pb-6">
|
||||
<div class="hidden md:flex items-start justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">{{ t('apps.title') }}</h1>
|
||||
<p class="text-white/70">{{ t('apps.subtitle') }}</p>
|
||||
</div>
|
||||
<!-- Desktop: tabs + search in one row -->
|
||||
<div class="hidden md:flex items-center gap-4 mb-4">
|
||||
<div class="mode-switcher flex-shrink-0">
|
||||
<RouterLink to="/dashboard/apps" class="mode-switcher-btn mode-switcher-btn-active">My Apps</RouterLink>
|
||||
<RouterLink to="/dashboard/marketplace" class="mode-switcher-btn">App Store</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="mb-4">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="t('apps.searchPlaceholder')"
|
||||
:aria-label="t('apps.searchLabel')"
|
||||
class="w-full px-4 py-3 md:py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||
class="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Mobile: search only (tabs are in Dashboard tab bar) -->
|
||||
<div class="md:hidden mb-4">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="t('apps.searchPlaceholder')"
|
||||
:aria-label="t('apps.searchLabel')"
|
||||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -175,7 +179,7 @@
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
@click="closeUninstallModal()"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div class="absolute inset-0 bg-black/10 backdrop-blur-md"></div>
|
||||
<div
|
||||
ref="uninstallModalRef"
|
||||
@click.stop
|
||||
@@ -260,7 +264,6 @@ function showActionError(msg: string) {
|
||||
|
||||
// Web-only app IDs and their URLs
|
||||
const WEB_ONLY_APP_URLS: Record<string, string> = {
|
||||
'indeedhub': 'https://archipelago.indeehub.studio',
|
||||
'botfights': 'https://botfights.net',
|
||||
'nwnn': 'https://nwnn.l484.com',
|
||||
'484-kitchen': 'https://484.kitchen',
|
||||
@@ -276,11 +279,6 @@ function isWebOnlyApp(id: string): boolean {
|
||||
|
||||
// Web-only apps (no container) — always show as installed bookmarks
|
||||
const WEB_ONLY_APPS: Record<string, PackageDataEntry> = {
|
||||
'indeedhub': {
|
||||
state: 'running' as PackageState,
|
||||
manifest: { id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: { short: 'Bitcoin documentary streaming platform', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
|
||||
'static-files': { license: '', instructions: '', icon: '/assets/img/app-icons/indeehub.ico' },
|
||||
},
|
||||
'botfights': {
|
||||
state: 'running' as PackageState,
|
||||
manifest: { id: 'botfights', title: 'BotFights', version: '1.0.0', description: { short: 'AI bot arena — build, train, and battle autonomous agents', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
|
||||
|
||||
@@ -69,9 +69,9 @@ const aiuiAvailable = ref<boolean | null>(null) // null = checking, true/false =
|
||||
|
||||
const aiuiUrl = computed(() => {
|
||||
const envUrl = import.meta.env.VITE_AIUI_URL
|
||||
if (envUrl) return `${envUrl}?embedded=true`
|
||||
if (envUrl) return `${envUrl}?embedded=true&hideClose=true`
|
||||
// In production, only return the URL if we've confirmed AIUI files exist
|
||||
if (import.meta.env.PROD && aiuiAvailable.value === true) return `/aiui/?embedded=true&v=${Date.now()}`
|
||||
if (import.meta.env.PROD && aiuiAvailable.value === true) return `/aiui/?embedded=true&hideClose=true&v=${Date.now()}`
|
||||
return ''
|
||||
})
|
||||
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
<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>
|
||||
<div class="pb-6">
|
||||
|
||||
<!-- Content Type Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@@ -56,12 +48,48 @@
|
||||
<span v-else-if="sectionCounts[section.id] !== undefined" class="text-white/30">{{ sectionCounts[section.id] }} items</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Peer Files Card -->
|
||||
<!-- Individual Peer Cards -->
|
||||
<div
|
||||
v-for="peer in peerNodes"
|
||||
:key="peer.did"
|
||||
data-controller-container
|
||||
tabindex="0"
|
||||
class="glass-card p-6 cursor-pointer transition-all hover:-translate-y-1 hover:bg-white/10"
|
||||
@click="router.push({ name: 'peer-files' })"
|
||||
@click="router.push({ name: 'peer-files', params: { peerId: peer.onion } })"
|
||||
>
|
||||
<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 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="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-white mb-0.5 truncate">{{ peer.name || truncateDid(peer.did) }}</h3>
|
||||
<p class="text-xs text-white/50 truncate">{{ peer.onion }}</p>
|
||||
</div>
|
||||
<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>
|
||||
<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="peer.trust_level === 'trusted' ? 'bg-green-500/15 text-green-400' : 'bg-purple-500/15 text-purple-400'"
|
||||
>
|
||||
<span class="w-1.5 h-1.5 rounded-full" :class="peer.trust_level === 'trusted' ? 'bg-green-400' : 'bg-purple-400'"></span>
|
||||
{{ peer.trust_level }}
|
||||
</span>
|
||||
<span class="text-white/30">Peer Node</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Peers placeholder (only if no peers found) -->
|
||||
<div
|
||||
v-if="!peersLoading && peerNodes.length === 0"
|
||||
data-controller-container
|
||||
tabindex="0"
|
||||
class="glass-card p-6 cursor-pointer transition-all hover:-translate-y-1 hover:bg-white/10"
|
||||
@click="router.push('/dashboard/server/federation')"
|
||||
>
|
||||
<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 bg-purple-500/15">
|
||||
@@ -71,26 +99,17 @@
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-white mb-0.5 truncate">Peer Files</h3>
|
||||
<p class="text-xs text-white/50">Browse files shared by federated nodes</p>
|
||||
<p class="text-xs text-white/50">Set up federation to share files with peers</p>
|
||||
</div>
|
||||
<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>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<template v-if="hasFederatedPeers">
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full bg-purple-500/15 text-purple-400">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-purple-400"></span>
|
||||
{{ peerCount }} peers
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full bg-white/5 text-white/40">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-white/30"></span>
|
||||
No peers yet
|
||||
</span>
|
||||
<span class="text-white/30">Set up federation to share files</span>
|
||||
</template>
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full bg-white/5 text-white/40">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-white/30"></span>
|
||||
No peers yet
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,8 +135,17 @@ const router = useRouter()
|
||||
const store = useAppStore()
|
||||
const sectionCounts = ref<Record<string, number>>({})
|
||||
const countsLoading = ref(false)
|
||||
const peerCount = ref(0)
|
||||
const hasFederatedPeers = computed(() => peerCount.value > 0)
|
||||
|
||||
interface PeerNode {
|
||||
did: string
|
||||
pubkey: string
|
||||
onion: string
|
||||
name?: string
|
||||
trust_level: string
|
||||
}
|
||||
|
||||
const peerNodes = ref<PeerNode[]>([])
|
||||
const peersLoading = ref(true)
|
||||
|
||||
const APP_ALIASES: Record<string, string[]> = {
|
||||
immich: ['immich_server', 'immich-server'],
|
||||
@@ -224,18 +252,26 @@ async function loadCounts() {
|
||||
|
||||
onMounted(() => {
|
||||
loadCounts()
|
||||
loadPeerCount()
|
||||
loadPeers()
|
||||
})
|
||||
|
||||
async function loadPeerCount() {
|
||||
async function loadPeers() {
|
||||
peersLoading.value = true
|
||||
try {
|
||||
const result = await rpcClient.federationListNodes()
|
||||
peerCount.value = result?.nodes?.length ?? 0
|
||||
peerNodes.value = result?.nodes ?? []
|
||||
} catch {
|
||||
peerCount.value = 0
|
||||
peerNodes.value = []
|
||||
} finally {
|
||||
peersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function truncateDid(did: string): string {
|
||||
if (did.length <= 24) return did
|
||||
return did.slice(0, 16) + '...' + did.slice(-8)
|
||||
}
|
||||
|
||||
function openSection(section: ContentSection) {
|
||||
router.push({ name: 'cloud-folder', params: { folderId: section.id } })
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="hidden md:block">
|
||||
<h1 class="text-2xl font-bold text-white">{{ section?.name || 'Folder' }}</h1>
|
||||
<p class="text-sm text-white/50">{{ section?.description }}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="pb-6">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<button @click="$router.push('/dashboard/web5')" class="glass-button glass-button-sm px-3 py-1.5 text-sm">
|
||||
@@ -115,7 +115,7 @@
|
||||
<!-- Credential Detail Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="selectedCredential" class="fixed inset-0 z-50 flex items-center justify-center p-4" @click.self="selectedCredential = null">
|
||||
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div class="fixed inset-0 bg-black/10 backdrop-blur-md"></div>
|
||||
<div class="relative glass-card p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-white">Credential Details</h3>
|
||||
|
||||
@@ -52,10 +52,9 @@
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<!-- Background overlay - 0.3 opacity default, 0.8 opacity for Web5, Network, and Settings -->
|
||||
<div
|
||||
class="fixed inset-0 transition-opacity duration-500 pointer-events-none"
|
||||
:class="showDarkOverlay ? 'bg-black/80' : 'bg-black/30'"
|
||||
<!-- Background overlay - uniform 0.2 opacity -->
|
||||
<div
|
||||
class="fixed inset-0 pointer-events-none bg-black/20"
|
||||
style="z-index: -5;"
|
||||
/>
|
||||
|
||||
@@ -149,7 +148,7 @@
|
||||
<main
|
||||
id="main-content"
|
||||
data-controller-zone="main"
|
||||
class="flex-1 overflow-hidden relative pb-20 md:pb-0 glass-piece z-10"
|
||||
class="flex-1 overflow-hidden relative pb-0 glass-piece z-10"
|
||||
:class="{ 'glass-throw-main': showZoomIn }"
|
||||
>
|
||||
<div data-controller-main-entry class="absolute top-4 right-4 md:top-6 md:right-8 z-20">
|
||||
@@ -277,14 +276,16 @@
|
||||
<div
|
||||
v-else
|
||||
:class="[
|
||||
'px-4 pt-4 md:pt-8 md:px-8 overflow-y-auto h-full',
|
||||
'absolute inset-0 px-4 pt-4 md:pt-8 md:px-8 overflow-y-auto',
|
||||
needsMobileBackButtonSpace
|
||||
? 'pb-[calc(var(--mobile-tab-bar-height,_72px)+96px)] md:pb-24'
|
||||
: 'pb-[calc(var(--mobile-tab-bar-height,_72px)+48px)] md:pb-24'
|
||||
? 'mobile-scroll-pad-back'
|
||||
: 'mobile-scroll-pad'
|
||||
]"
|
||||
:style="mobileTabPaddingTop ? { paddingTop: (mobileTabPaddingTop + 16) + 'px' } : undefined"
|
||||
>
|
||||
<component :is="Component" class="view-container" />
|
||||
<component :is="Component" class="view-container flex-none" />
|
||||
<!-- Bottom spacer — scroll clearance on all pages -->
|
||||
<div class="shrink-0 h-6 md:h-12" aria-hidden="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -846,6 +847,10 @@ function getTransitionName(currentRoute: RouteLocationNormalizedLoaded) {
|
||||
} else if (wasAppDetails && isMarketplaceList) {
|
||||
transitionName = 'depth-back'
|
||||
}
|
||||
// Desktop: no transition between Apps ↔ Marketplace (same-page tab feel)
|
||||
else if ((wasAppsList && isMarketplaceList) || (wasMarketplaceList && isAppsList)) {
|
||||
transitionName = 'fade'
|
||||
}
|
||||
// Vertical transition: between main tabs (desktop)
|
||||
else {
|
||||
const currentIndex = tabOrder.indexOf(currentPath)
|
||||
@@ -1278,8 +1283,7 @@ aside:not(.sidebar-animate) .sidebar-logout-btn {
|
||||
}
|
||||
|
||||
.view-container {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
/* No forced height — content sizes naturally, spacer below provides clearance */
|
||||
}
|
||||
|
||||
/* Forward transition: 2advanced fluid depth */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="pb-6">
|
||||
<div class="mb-8">
|
||||
<button
|
||||
@click="router.push('/dashboard/web5')"
|
||||
@@ -195,7 +195,7 @@
|
||||
</template>
|
||||
|
||||
<!-- Node Detail Modal -->
|
||||
<div v-if="selectedNode" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="selectedNode = null; confirmRemove = false">
|
||||
<div v-if="selectedNode" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md" @click.self="selectedNode = null; confirmRemove = false">
|
||||
<div class="glass-card p-6 w-full max-w-lg max-h-[80vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-white">Node Details</h2>
|
||||
@@ -322,7 +322,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Join Modal -->
|
||||
<div v-if="showJoinModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="showJoinModal = false">
|
||||
<div v-if="showJoinModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md" @click.self="showJoinModal = false">
|
||||
<div class="glass-card p-6 w-full max-w-md">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-white">Join Federation</h2>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="pb-6">
|
||||
<div class="mb-4 md:mb-8 flex items-start justify-between gap-4">
|
||||
<div class="min-h-[4.5rem]">
|
||||
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">
|
||||
|
||||
@@ -109,7 +109,7 @@ const launchableApps = computed<KioskApp[]>(() => {
|
||||
'fedimint-gateway': '/app/fedimint-gateway/',
|
||||
'dwn': '/app/dwn/',
|
||||
'nostr-rs-relay': '/app/nostr-rs-relay/',
|
||||
'indeedhub': 'https://archipelago.indeehub.studio',
|
||||
'indeedhub': 'http://localhost:8190',
|
||||
'botfights': 'https://botfights.net',
|
||||
'nwnn': 'https://nwnn.l484.com',
|
||||
'484-kitchen': 'https://484.kitchen',
|
||||
|
||||
@@ -74,20 +74,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex mb-8 items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">{{ t('marketplace.title') }}</h1>
|
||||
<p class="text-white/70">{{ t('marketplace.subtitle') }}</p>
|
||||
</div>
|
||||
<!-- Desktop: tabs + categories + search in one row -->
|
||||
<div class="hidden md:flex mb-4 items-center gap-4">
|
||||
<div class="mode-switcher flex-shrink-0">
|
||||
<RouterLink to="/dashboard/apps" class="mode-switcher-btn">My Apps</RouterLink>
|
||||
<RouterLink to="/dashboard/marketplace" class="mode-switcher-btn mode-switcher-btn-active">App Store</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Tabs + Search (Desktop only) -->
|
||||
<div class="hidden md:flex mb-6 items-center justify-between gap-4">
|
||||
<div class="mode-switcher">
|
||||
<div class="mode-switcher flex-shrink-0">
|
||||
<button
|
||||
v-for="category in categoriesWithApps"
|
||||
:key="category.id"
|
||||
@@ -104,7 +97,7 @@
|
||||
type="text"
|
||||
:placeholder="t('marketplace.searchPlaceholder')"
|
||||
:aria-label="t('marketplace.searchApps')"
|
||||
class="flex-shrink-0 w-64 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||
class="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -259,7 +252,7 @@
|
||||
<Transition name="modal">
|
||||
<div
|
||||
v-if="showFilterModal"
|
||||
class="fixed inset-0 z-50 flex items-end justify-center md:hidden bg-black/60 backdrop-blur-sm"
|
||||
class="fixed inset-0 z-50 flex items-end justify-center md:hidden bg-black/10 backdrop-blur-md"
|
||||
@click.self="closeFilterModal()"
|
||||
>
|
||||
<div ref="filterModalRef" class="glass-card p-6 w-full rounded-t-3xl max-h-[80vh] overflow-y-auto">
|
||||
@@ -648,7 +641,6 @@ function isInstalled(appId: string): boolean {
|
||||
|
||||
/** Web-only apps — external URLs with no container */
|
||||
const WEB_ONLY_APP_URLS: Record<string, string> = {
|
||||
'indeedhub': 'https://archipelago.indeehub.studio',
|
||||
'botfights': 'https://botfights.net',
|
||||
'nwnn': 'https://nwnn.l484.com',
|
||||
'484-kitchen': 'https://484.kitchen',
|
||||
@@ -685,6 +677,7 @@ const APP_LAUNCH_URLS: Record<string, string> = {
|
||||
'fedimint': 'http://localhost:8175',
|
||||
'nostr-rs-relay': 'http://localhost:18081',
|
||||
'dwn': 'http://localhost:3100',
|
||||
'indeedhub': 'http://localhost:8190',
|
||||
}
|
||||
|
||||
function launchInstalledApp(app: MarketplaceApp) {
|
||||
@@ -995,7 +988,7 @@ function getCuratedAppList() {
|
||||
id: 'indeedhub',
|
||||
title: 'Indeehub',
|
||||
version: '0.1.0',
|
||||
description: 'Bitcoin documentary streaming platform. Stream God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology.',
|
||||
description: 'Bitcoin documentary streaming platform with Nostr identity sign-in. Stream God Bless Bitcoin and other educational content about sovereignty and decentralized technology.',
|
||||
icon: '/assets/img/app-icons/indeehub.ico',
|
||||
author: 'Indeehub Team',
|
||||
dockerImage: 'localhost/indeedhub:latest',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="pb-6">
|
||||
<div class="hidden md:block mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
|
||||
+128
-126
@@ -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) {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="hidden md:block mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Network</h1>
|
||||
<p class="text-white/70">Manage your network infrastructure and Web3 services</p>
|
||||
<p class="text-sm text-white/60 mt-2">{{ connectedNodes }} connected nodes</p>
|
||||
</div>
|
||||
<div class="pb-6">
|
||||
|
||||
<!-- Disk Space Warning Banner -->
|
||||
<div
|
||||
@@ -53,7 +48,7 @@
|
||||
</div>
|
||||
<button
|
||||
@click="restartServices"
|
||||
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
||||
class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50 flex items-center justify-center"
|
||||
:disabled="restarting"
|
||||
>
|
||||
{{ restarting ? 'Restarting...' : 'Restart' }}
|
||||
@@ -74,7 +69,7 @@
|
||||
</div>
|
||||
<button
|
||||
@click="checkConnectivity"
|
||||
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
||||
class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50 flex items-center justify-center"
|
||||
:disabled="checkingConnectivity"
|
||||
>
|
||||
{{ checkingConnectivity ? 'Checking...' : 'Check' }}
|
||||
@@ -83,25 +78,27 @@
|
||||
|
||||
<!-- Auto-Sync Toggle -->
|
||||
<div data-controller-container tabindex="0" class="flex flex-col gap-3 p-4 bg-white/5 rounded-lg min-w-0">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<svg class="w-5 h-5 text-white/60 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
|
||||
</svg>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium text-white">Auto-Sync</p>
|
||||
<p class="text-xs text-white/60">{{ autoSyncEnabled ? 'Enabled' : 'Disabled' }}</p>
|
||||
<div class="flex items-center justify-between min-w-0">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<svg class="w-5 h-5 text-white/60 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
|
||||
</svg>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium text-white">Auto-Sync</p>
|
||||
<p class="text-xs text-white/60">{{ autoSyncEnabled ? 'Enabled' : 'Disabled' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="toggleAutoSync"
|
||||
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors shrink-0"
|
||||
:class="autoSyncEnabled ? 'bg-green-500' : 'bg-white/20'"
|
||||
>
|
||||
<span
|
||||
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform shadow"
|
||||
:class="autoSyncEnabled ? 'translate-x-7' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="toggleAutoSync"
|
||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors self-start"
|
||||
:class="autoSyncEnabled ? 'bg-green-500' : 'bg-white/20'"
|
||||
>
|
||||
<span
|
||||
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||
:class="autoSyncEnabled ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Logs & Diagnostics -->
|
||||
@@ -117,7 +114,7 @@
|
||||
</div>
|
||||
<button
|
||||
@click="viewLogs"
|
||||
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
||||
class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors flex items-center justify-center"
|
||||
>
|
||||
View
|
||||
</button>
|
||||
@@ -128,7 +125,7 @@
|
||||
<!-- Overview Cards -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<!-- Local Network Card -->
|
||||
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col h-full min-h-0">
|
||||
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col">
|
||||
<div class="flex items-start gap-4 mb-4 shrink-0">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -220,13 +217,13 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<button disabled title="Coming Soon" class="mt-auto pt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium opacity-50 cursor-not-allowed shrink-0">
|
||||
<button disabled title="Coming Soon" class="mt-4 w-full min-h-[44px] glass-button rounded-lg text-sm font-medium opacity-50 cursor-not-allowed flex items-center justify-center">
|
||||
Manage Local Network
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Web3 Card -->
|
||||
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col h-full min-h-0">
|
||||
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col">
|
||||
<div class="flex items-start gap-4 mb-4 shrink-0">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -281,7 +278,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button disabled title="Coming Soon" class="mt-auto pt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium opacity-50 cursor-not-allowed shrink-0">
|
||||
<button disabled title="Coming Soon" class="mt-4 w-full min-h-[44px] glass-button rounded-lg text-sm font-medium opacity-50 cursor-not-allowed flex items-center justify-center">
|
||||
Manage Web3 Services
|
||||
</button>
|
||||
</div>
|
||||
@@ -379,7 +376,7 @@
|
||||
</div>
|
||||
|
||||
<!-- WiFi Scan Modal -->
|
||||
<div v-if="showWifiModal" class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4" @click.self="showWifiModal = false">
|
||||
<div v-if="showWifiModal" class="fixed inset-0 bg-black/10 backdrop-blur-md z-50 flex items-center justify-center p-4" @click.self="showWifiModal = false">
|
||||
<div class="glass-card p-6 w-full max-w-md">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-white">WiFi Networks</h3>
|
||||
@@ -439,7 +436,7 @@
|
||||
</div>
|
||||
|
||||
<!-- DNS Configuration Modal -->
|
||||
<div v-if="showDnsModal" class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4" @click.self="showDnsModal = false">
|
||||
<div v-if="showDnsModal" class="fixed inset-0 bg-black/10 backdrop-blur-md z-50 flex items-center justify-center p-4" @click.self="showDnsModal = false">
|
||||
<div class="glass-card p-6 w-full max-w-md">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-white">DNS Configuration</h3>
|
||||
@@ -822,7 +819,7 @@ function copyTorAddress(address: string) {
|
||||
|
||||
async function toggleTorApp(appId: string, enabled: boolean) {
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.toggle-service', params: { name: appId, enabled } })
|
||||
await rpcClient.call({ method: 'tor.toggle-app', params: { app_id: appId, enabled } })
|
||||
await loadTorServices()
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to toggle Tor app:', e)
|
||||
@@ -832,7 +829,7 @@ async function toggleTorApp(appId: string, enabled: boolean) {
|
||||
async function rotateNodeAddress() {
|
||||
torRotating.value = true
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.rotate-address', params: { name: 'archipelago' } })
|
||||
await rpcClient.call({ method: 'tor.rotate-service', params: { name: 'archipelago' } })
|
||||
await loadTorServices()
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to rotate Tor address:', e)
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-8 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">{{ t('settings.title') }}</h1>
|
||||
<p class="text-white/80">{{ t('settings.subtitle') }}</p>
|
||||
</div>
|
||||
<!-- Controller indicator - Mobile only (desktop shows in sidebar) -->
|
||||
<div class="md:hidden">
|
||||
<ControllerIndicator />
|
||||
</div>
|
||||
<div class="pb-6">
|
||||
<!-- Controller indicator - Mobile only (desktop shows in sidebar) -->
|
||||
<div class="md:hidden mb-4">
|
||||
<ControllerIndicator />
|
||||
</div>
|
||||
|
||||
<!-- Account Section -->
|
||||
@@ -102,26 +96,24 @@
|
||||
|
||||
<!-- Tor / Onion Address (below DID, with copy button) -->
|
||||
<div v-if="serverTorAddress" :class="userDid ? 'pt-4 border-t border-white/10' : ''">
|
||||
<div class="flex items-center justify-between gap-2 mb-2">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<svg class="w-5 h-5 text-amber-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">{{ t('settings.onionAddress') }}</p>
|
||||
</div>
|
||||
<button
|
||||
@click="copyOnionAddress"
|
||||
class="shrink-0 px-3 py-1.5 rounded-lg glass-button glass-button-sm text-xs font-medium text-white/90 hover:text-white transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<svg v-if="!copiedOnion" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span v-else class="text-green-400 text-xs">{{ t('common.copied') }}</span>
|
||||
<span v-if="!copiedOnion">{{ t('common.copy') }}</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<svg class="w-5 h-5 text-amber-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">{{ t('settings.onionAddress') }}</p>
|
||||
</div>
|
||||
<p class="text-sm font-mono text-amber-400/90 break-all" :title="serverTorAddress">{{ serverTorAddress }}</p>
|
||||
<p class="text-xs text-white/50 mt-1">{{ t('settings.onionHelper') }}</p>
|
||||
<p class="text-sm font-mono text-amber-400/90 break-all mb-1" :title="serverTorAddress">{{ serverTorAddress }}</p>
|
||||
<p class="text-xs text-white/50 mb-3">{{ t('settings.onionHelper') }}</p>
|
||||
<button
|
||||
@click="copyOnionAddress"
|
||||
class="w-full min-h-[44px] rounded-lg glass-button text-sm font-medium text-white/90 hover:text-white transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg v-if="!copiedOnion" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span v-if="!copiedOnion">{{ t('common.copy') }}</span>
|
||||
<span v-else class="text-green-400">{{ t('common.copied') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,7 +135,7 @@
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showChangePasswordModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md"
|
||||
@click.self="closeChangePasswordModal()"
|
||||
>
|
||||
<div ref="changePasswordModalRef" class="glass-card p-6 max-w-md w-full">
|
||||
@@ -255,7 +247,7 @@
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showTotpSetupModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md"
|
||||
@click.self="closeTotpSetup"
|
||||
@keydown.escape="closeTotpSetup"
|
||||
>
|
||||
@@ -361,7 +353,7 @@
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showTotpDisableModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md"
|
||||
@click.self="closeTotpDisable"
|
||||
@keydown.escape="closeTotpDisable"
|
||||
>
|
||||
@@ -507,7 +499,7 @@
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showClaudeLoginModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md"
|
||||
@click.self="showClaudeLoginModal = false"
|
||||
>
|
||||
<div class="glass-card p-0 max-w-lg w-full overflow-hidden" style="height: 480px">
|
||||
@@ -723,12 +715,10 @@
|
||||
|
||||
<!-- Backup & Restore Section -->
|
||||
<div class="glass-card px-6 py-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white/96">{{ t('settings.backup') }}</h2>
|
||||
<p class="text-sm text-white/60 mt-1">{{ t('settings.backupRestoreDesc') }}</p>
|
||||
</div>
|
||||
<button @click="showCreateBackupModal = true" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<div class="mb-4">
|
||||
<h2 class="text-xl font-semibold text-white/96 mb-1">{{ t('settings.backup') }}</h2>
|
||||
<p class="text-sm text-white/60 mb-3">{{ t('settings.backupRestoreDesc') }}</p>
|
||||
<button @click="showCreateBackupModal = true" class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -770,7 +760,7 @@
|
||||
|
||||
<!-- Create Backup Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showCreateBackupModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="showCreateBackupModal = false">
|
||||
<div v-if="showCreateBackupModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md" @click.self="showCreateBackupModal = false">
|
||||
<div class="glass-card p-6 w-full max-w-md" role="dialog" aria-modal="true" aria-labelledby="create-backup-title">
|
||||
<h3 id="create-backup-title" class="text-lg font-semibold text-white mb-4">{{ t('settings.createEncryptedBackup') }}</h3>
|
||||
<div class="space-y-3">
|
||||
@@ -795,7 +785,7 @@
|
||||
|
||||
<!-- Restore Backup Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showRestoreModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="showRestoreModal = false">
|
||||
<div v-if="showRestoreModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md" @click.self="showRestoreModal = false">
|
||||
<div class="glass-card p-6 w-full max-w-md" role="dialog" aria-modal="true" aria-labelledby="restore-backup-title">
|
||||
<h3 id="restore-backup-title" class="text-lg font-semibold text-white mb-2">{{ t('settings.restoreBackupTitle') }}</h3>
|
||||
<p class="text-sm text-red-400/80 mb-4">{{ t('settings.restoreWarning') }}</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="pb-6">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">{{ t('systemUpdate.title') }}</h1>
|
||||
<p class="text-white/70">{{ t('systemUpdate.subtitle') }}</p>
|
||||
@@ -166,7 +166,7 @@
|
||||
|
||||
<!-- Confirmation modal -->
|
||||
<Transition name="fade">
|
||||
<div v-if="confirmAction" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="cancelConfirm">
|
||||
<div v-if="confirmAction" class="fixed inset-0 z-50 flex items-center justify-center bg-black/10 backdrop-blur-md" @click.self="cancelConfirm">
|
||||
<div class="glass-card p-6 max-w-sm w-full mx-4">
|
||||
<h3 class="text-lg font-semibold text-white mb-3">
|
||||
{{ confirmAction === 'apply' ? t('systemUpdate.applyTitle') : t('systemUpdate.rollbackTitle') }}
|
||||
|
||||
+177
-70
@@ -1,14 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">{{ t('web5.title') }}</h1>
|
||||
<p class="text-white/70">{{ t('web5.subtitle') }}</p>
|
||||
<p class="text-sm text-white/60 mt-2">{{ t('web5.profitsHelper') }}</p>
|
||||
</div>
|
||||
<div class="pb-6">
|
||||
|
||||
<!-- Quick Actions Container -->
|
||||
<div class="glass-card p-6 mb-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 stagger-grid">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-6 gap-4 stagger-grid">
|
||||
<!-- Networking Profits -->
|
||||
<div data-controller-container tabindex="0" class="card-stagger flex flex-col gap-3 p-4 bg-white/5 rounded-lg min-w-0" style="--stagger-index: 0">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
@@ -188,8 +183,8 @@
|
||||
|
||||
<!-- DID Document Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showDidDocModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="showDidDocModal = false" @keydown.escape="showDidDocModal = false">
|
||||
<div class="glass-card p-6 max-w-lg w-full max-h-[90vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="did-doc-title">
|
||||
<div v-if="showDidDocModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="showDidDocModal = false" @keydown.escape="showDidDocModal = false">
|
||||
<div class="glass-card p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="did-doc-title">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 id="did-doc-title" class="text-lg font-semibold text-white">{{ t('web5.didDocument') }}</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -222,7 +217,7 @@
|
||||
|
||||
<!-- Send Message Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showSendMessageModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="closeSendMessageModal()">
|
||||
<div v-if="showSendMessageModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="closeSendMessageModal()">
|
||||
<div ref="sendMessageModalRef" class="glass-card p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">{{ t('web5.sendMessageTitle') }}</h3>
|
||||
<p class="text-white/70 text-sm mb-4">Messages are sent over the Tor network to the selected peer.</p>
|
||||
@@ -235,7 +230,7 @@
|
||||
>
|
||||
<option value="">{{ t('web5.selectPeer') }}</option>
|
||||
<option v-for="p in peers" :key="p.pubkey" :value="p.onion">
|
||||
{{ p.name || p.onion || p.pubkey.slice(0, 12) + '...' }}
|
||||
{{ p.name || p.onion || (p.pubkey || '').slice(0, 12) + '...' }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -270,10 +265,10 @@
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Core Services Overview Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Core Services Overview Cards — Row 1 -->
|
||||
<div class="flex flex-col md:flex-row gap-6 mb-6">
|
||||
<!-- Bitcoin Domain Name Portfolio -->
|
||||
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col h-full min-h-0" style="--stagger-index: 0">
|
||||
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col md:w-1/2" style="--stagger-index: 0">
|
||||
<div class="flex items-start gap-4 mb-4 shrink-0">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -320,13 +315,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="showDomainsModal = true" class="mt-auto pt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors shrink-0">
|
||||
<button @click="showDomainsModal = true" class="mt-6 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors shrink-0">
|
||||
{{ t('web5.manageDomains') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Wallet -->
|
||||
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col h-full min-h-0" style="--stagger-index: 1">
|
||||
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col md:w-1/2" style="--stagger-index: 1">
|
||||
<div class="flex items-start gap-4 mb-4 shrink-0">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -389,9 +384,12 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Core Services Overview Cards — Row 2 -->
|
||||
<div class="flex flex-col md:flex-row gap-6 mb-8">
|
||||
<!-- Nostr Relays -->
|
||||
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col h-full min-h-0" style="--stagger-index: 2">
|
||||
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col md:w-1/2" style="--stagger-index: 2">
|
||||
<div class="flex items-start gap-4 mb-4 shrink-0">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -438,13 +436,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="showRelaysModal = true" class="mt-auto pt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors shrink-0">
|
||||
<button @click="showRelaysModal = true" class="mt-6 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors shrink-0">
|
||||
{{ t('web5.relays') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Node Visibility -->
|
||||
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col h-full min-h-0" style="--stagger-index: 3">
|
||||
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col md:w-1/2" style="--stagger-index: 3">
|
||||
<div class="flex items-start gap-4 mb-4 shrink-0">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -508,10 +506,12 @@
|
||||
{{ t('web5.discoverableWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connected Nodes (P2P over Tor) -->
|
||||
<div ref="nodesContainerRef" data-controller-container tabindex="0" class="glass-card p-6 lg:col-span-4 scroll-mt-24">
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<!-- Connected Nodes (P2P over Tor) -->
|
||||
<div ref="nodesContainerRef" data-controller-container tabindex="0" class="glass-card p-6 mb-8 scroll-mt-24">
|
||||
<!-- Desktop: side-by-side layout -->
|
||||
<div class="hidden md:flex items-start gap-4 mb-4">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
@@ -536,6 +536,32 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile: stacked layout -->
|
||||
<div class="md:hidden mb-4">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white">{{ t('web5.connectedNodes') }}</h2>
|
||||
</div>
|
||||
<p class="text-white/70 text-sm mb-3">{{ t('web5.peerNodesDescription') }}</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
@click="router.push('/dashboard/server/federation')"
|
||||
class="min-h-[44px] glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors flex items-center justify-center"
|
||||
>
|
||||
{{ t('web5.findNodes') }}
|
||||
</button>
|
||||
<button
|
||||
@click="loadPeers"
|
||||
class="min-h-[44px] glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors flex items-center justify-center"
|
||||
>
|
||||
{{ loadingPeers ? '...' : t('common.refresh') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs: Peers | Messages | Requests -->
|
||||
<div class="flex gap-1 mb-4 border-b border-white/10">
|
||||
@@ -580,7 +606,7 @@
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="w-2 h-2 rounded-full shrink-0" :class="peerReachable[p.onion] ? 'bg-green-400' : 'bg-amber-400'"></div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-mono text-white/90 truncate">{{ p.name || p.onion || p.pubkey.slice(0, 16) + '...' }}</p>
|
||||
<p class="text-sm font-mono text-white/90 truncate">{{ p.name || p.onion || (p.pubkey || '').slice(0, 16) + '...' }}</p>
|
||||
<p class="text-xs text-white/50 truncate">{{ p.onion }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -607,7 +633,7 @@
|
||||
class="p-3 bg-white/5 rounded-lg border-l-2 border-orange-500/50"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2 mb-1">
|
||||
<p class="text-xs font-mono text-white/60 truncate" :title="m.from_pubkey">{{ m.from_pubkey.slice(0, 16) }}...</p>
|
||||
<p class="text-xs font-mono text-white/60 truncate" :title="m.from_pubkey">{{ (m.from_pubkey || '').slice(0, 16) }}...</p>
|
||||
<span class="text-xs text-white/40 shrink-0">{{ formatMessageTime(m.timestamp) }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-white/90 break-words">{{ m.message }}</p>
|
||||
@@ -678,11 +704,11 @@
|
||||
{{ loadingRequests ? t('common.loading') : t('web5.refreshRequests') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shared Content -->
|
||||
<div class="glass-card p-6 mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<!-- Desktop: side-by-side -->
|
||||
<div class="hidden md:flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -694,11 +720,34 @@
|
||||
<p class="text-xs text-white/60">{{ t('web5.contentDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button v-if="contentTab === 'mine'" @click="loadContentItems" :disabled="contentLoading" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium">
|
||||
<div v-if="contentTab === 'mine'" class="flex items-center gap-2">
|
||||
<button @click="loadContentItems" :disabled="contentLoading" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium">
|
||||
{{ contentLoading ? '...' : 'Refresh' }}
|
||||
</button>
|
||||
<button v-if="contentTab === 'mine'" @click="showAddContentModal = true" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium flex items-center gap-2">
|
||||
<button @click="showAddContentModal = true" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile: stacked -->
|
||||
<div class="md:hidden mb-4">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-white">{{ t('web5.content') }}</h2>
|
||||
</div>
|
||||
<p class="text-xs text-white/60 mb-3">{{ t('web5.contentDesc') }}</p>
|
||||
<div v-if="contentTab === 'mine'" class="grid grid-cols-2 gap-2">
|
||||
<button @click="loadContentItems" :disabled="contentLoading" class="glass-button min-h-[44px] rounded-lg text-sm font-medium flex items-center justify-center">
|
||||
{{ contentLoading ? '...' : 'Refresh' }}
|
||||
</button>
|
||||
<button @click="showAddContentModal = true" class="glass-button min-h-[44px] rounded-lg text-sm font-medium flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -822,7 +871,7 @@
|
||||
>
|
||||
<option value="">{{ t('web5.selectPeer') }}</option>
|
||||
<option v-for="p in peers" :key="p.pubkey" :value="p.onion">
|
||||
{{ p.name || p.onion || p.pubkey.slice(0, 12) + '...' }}
|
||||
{{ p.name || p.onion || (p.pubkey || '').slice(0, 12) + '...' }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
@@ -912,7 +961,7 @@
|
||||
|
||||
<!-- Content Streaming Player -->
|
||||
<Teleport to="body">
|
||||
<div v-if="streamingItem" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" @click.self="closePlayer" @keydown.escape="closePlayer">
|
||||
<div v-if="streamingItem" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="closePlayer" @keydown.escape="closePlayer">
|
||||
<div class="glass-card p-0 w-full max-w-2xl overflow-hidden" role="dialog" aria-modal="true">
|
||||
<!-- Player Header -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-white/10">
|
||||
@@ -984,7 +1033,7 @@
|
||||
|
||||
<!-- Add Content Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showAddContentModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showAddContentModal = false" @keydown.escape="showAddContentModal = false">
|
||||
<div v-if="showAddContentModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="showAddContentModal = false" @keydown.escape="showAddContentModal = false">
|
||||
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="add-content-title">
|
||||
<h2 id="add-content-title" class="text-lg font-bold text-white mb-4">{{ t('web5.addContentTitle') }}</h2>
|
||||
<div class="space-y-4">
|
||||
@@ -1035,7 +1084,8 @@
|
||||
|
||||
<!-- Identity Management -->
|
||||
<div class="glass-card p-6 mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<!-- Desktop: side-by-side -->
|
||||
<div class="hidden md:flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -1054,6 +1104,24 @@
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
<!-- Mobile: stacked -->
|
||||
<div class="md:hidden mb-4">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-white">{{ t('web5.identities') }}</h2>
|
||||
</div>
|
||||
<p class="text-xs text-white/60 mb-3">{{ t('web5.identitiesDesc') }}</p>
|
||||
<button @click="showCreateIdentityModal = true" class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="identitiesLoading" class="py-6 text-center">
|
||||
@@ -1112,7 +1180,7 @@
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<button @click="copyIdentityDid(identity.did)" class="p-2 rounded-lg text-white/50 hover:text-white hover:bg-white/10 transition-colors" title="Copy DID">
|
||||
<button @click="copyIdentityDid(identity.did)" class="p-2 rounded-lg text-white/50 hover:text-white hover:bg-white/10 transition-colors" title="Copy">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
||||
</svg>
|
||||
@@ -1133,7 +1201,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Create Identity Modal -->
|
||||
<div v-if="showCreateIdentityModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showCreateIdentityModal = false" @keydown.escape="showCreateIdentityModal = false">
|
||||
<div v-if="showCreateIdentityModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="showCreateIdentityModal = false" @keydown.escape="showCreateIdentityModal = false">
|
||||
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="create-identity-title">
|
||||
<h2 id="create-identity-title" class="text-lg font-bold text-white mb-4">{{ t('web5.createIdentityTitle') }}</h2>
|
||||
<div class="space-y-4">
|
||||
@@ -1167,7 +1235,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div v-if="deleteIdentityTarget" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="deleteIdentityTarget = null" @keydown.escape="deleteIdentityTarget = null">
|
||||
<div v-if="deleteIdentityTarget" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="deleteIdentityTarget = null" @keydown.escape="deleteIdentityTarget = null">
|
||||
<div class="glass-card p-6 w-full max-w-sm mx-4" role="dialog" aria-modal="true" aria-labelledby="delete-identity-title">
|
||||
<h2 id="delete-identity-title" class="text-lg font-bold text-white mb-2">{{ t('web5.deleteIdentityTitle') }}</h2>
|
||||
<p class="text-white/60 text-sm mb-4">{{ t('web5.deleteIdentityConfirm') }}</p>
|
||||
@@ -1180,7 +1248,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Unified Send Modal -->
|
||||
<div v-if="showUnifiedSendModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="closeUnifiedSendModal" @keydown.escape="closeUnifiedSendModal">
|
||||
<div v-if="showUnifiedSendModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="closeUnifiedSendModal" @keydown.escape="closeUnifiedSendModal">
|
||||
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="send-bitcoin-title">
|
||||
<h2 id="send-bitcoin-title" class="text-lg font-bold text-white mb-4">{{ t('web5.sendBitcoinTitle') }}</h2>
|
||||
|
||||
@@ -1280,7 +1348,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Unified Receive Modal -->
|
||||
<div v-if="showUnifiedReceiveModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="closeUnifiedReceiveModal" @keydown.escape="closeUnifiedReceiveModal">
|
||||
<div v-if="showUnifiedReceiveModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="closeUnifiedReceiveModal" @keydown.escape="closeUnifiedReceiveModal">
|
||||
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="receive-bitcoin-title">
|
||||
<h2 id="receive-bitcoin-title" class="text-lg font-bold text-white mb-4">{{ t('web5.receiveBitcoinTitle') }}</h2>
|
||||
|
||||
@@ -1470,7 +1538,7 @@
|
||||
<div v-else class="space-y-2 max-h-64 overflow-y-auto">
|
||||
<div v-for="msg in dwnMessages" :key="msg.record_id" class="bg-white/5 rounded-lg p-3">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs font-mono text-white/50 truncate max-w-[200px]" :title="msg.record_id">{{ msg.record_id.slice(0, 8) }}...</span>
|
||||
<span class="text-xs font-mono text-white/50 truncate max-w-[200px]" :title="msg.record_id">{{ (msg.record_id || '').slice(0, 8) }}...</span>
|
||||
<span class="text-xs text-white/40">{{ new Date(msg.date_created).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
@@ -1501,7 +1569,8 @@
|
||||
|
||||
<!-- Verifiable Credentials -->
|
||||
<div class="glass-card p-6 mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<!-- Desktop: side-by-side -->
|
||||
<div class="hidden md:flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -1517,6 +1586,21 @@
|
||||
Manage →
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- Mobile: stacked -->
|
||||
<div class="md:hidden mb-4">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-white">{{ t('web5.verifiableCredentials') }}</h2>
|
||||
</div>
|
||||
<p class="text-xs text-white/60 mb-3">{{ t('web5.verifiableCredentialsDesc') }}</p>
|
||||
<router-link to="/dashboard/web5/credentials" class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium flex items-center justify-center gap-2">
|
||||
Manage →
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-3 gap-3 mb-4">
|
||||
@@ -1539,7 +1623,7 @@
|
||||
<div v-for="vc in vcCredentials.slice(0, 3)" :key="vc.id" class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm text-white font-medium">{{ vc.type }}</div>
|
||||
<div class="text-xs text-white/50 truncate">To: {{ vc.subject.slice(0, 30) }}...</div>
|
||||
<div class="text-xs text-white/50 truncate">To: {{ (vc.subject || '').slice(0, 30) }}...</div>
|
||||
</div>
|
||||
<span :class="{
|
||||
'text-green-400': vc.status === 'active',
|
||||
@@ -1557,7 +1641,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Domains Management Modal -->
|
||||
<div v-if="showDomainsModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showDomainsModal = false" @keydown.escape="showDomainsModal = false">
|
||||
<div v-if="showDomainsModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="showDomainsModal = false" @keydown.escape="showDomainsModal = false">
|
||||
<div class="glass-card p-6 w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="domains-title">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 id="domains-title" class="text-lg font-bold text-white">{{ t('web5.domainsTitle') }}</h2>
|
||||
@@ -1604,7 +1688,7 @@
|
||||
<label class="text-white/60 text-xs block mb-1">Link to Identity</label>
|
||||
<select v-model="newDomainIdentityId" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-white/30">
|
||||
<option value="" disabled>Select identity...</option>
|
||||
<option v-for="id in managedIdentities" :key="id.id" :value="id.id">{{ id.name }} ({{ id.did.slice(0, 24) }}...)</option>
|
||||
<option v-for="id in managedIdentities" :key="id.id" :value="id.id">{{ id.name }} ({{ (id.did || '').slice(0, 24) }}...)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="domainError" class="text-xs text-red-400 mb-2">{{ domainError }}</div>
|
||||
@@ -1634,7 +1718,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Relay Management Modal -->
|
||||
<div v-if="showRelaysModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showRelaysModal = false" @keydown.escape="showRelaysModal = false">
|
||||
<div v-if="showRelaysModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="showRelaysModal = false" @keydown.escape="showRelaysModal = false">
|
||||
<div class="glass-card p-6 w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="relays-title">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 id="relays-title" class="text-lg font-bold text-white">{{ t('web5.nostrRelays') }}</h2>
|
||||
@@ -1908,18 +1992,27 @@ async function createDID() {
|
||||
localStorage.setItem('neode_did', res.did)
|
||||
} catch {
|
||||
// Fallback: generate a did:key locally using Web Crypto
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
)
|
||||
const exported = await crypto.subtle.exportKey('raw', keyPair.publicKey)
|
||||
const bytes = new Uint8Array(exported)
|
||||
// Multicodec prefix for P-256 public key (0x1200) + base58btc
|
||||
const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
const did = `did:key:z${hex}`
|
||||
storedDid.value = did
|
||||
localStorage.setItem('neode_did', did)
|
||||
if (!crypto.subtle) {
|
||||
// crypto.subtle requires HTTPS — generate random fallback
|
||||
const randomBytes = new Uint8Array(32)
|
||||
crypto.getRandomValues(randomBytes)
|
||||
const hex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
const did = `did:key:z${hex}`
|
||||
storedDid.value = did
|
||||
localStorage.setItem('neode_did', did)
|
||||
} else {
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
)
|
||||
const exported = await crypto.subtle.exportKey('raw', keyPair.publicKey)
|
||||
const bytes = new Uint8Array(exported)
|
||||
const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
const did = `did:key:z${hex}`
|
||||
storedDid.value = did
|
||||
localStorage.setItem('neode_did', did)
|
||||
}
|
||||
} finally {
|
||||
creatingDid.value = false
|
||||
}
|
||||
@@ -1965,7 +2058,7 @@ async function refreshDhtDid() {
|
||||
|
||||
async function copyDhtDid() {
|
||||
if (!dhtDid.value) return
|
||||
await navigator.clipboard.writeText(dhtDid.value)
|
||||
await safeClipboardWrite(dhtDid.value)
|
||||
dhtDidCopied.value = true
|
||||
setTimeout(() => { dhtDidCopied.value = false }, 2000)
|
||||
}
|
||||
@@ -1977,7 +2070,7 @@ try {
|
||||
|
||||
async function copyDid() {
|
||||
if (!userDid.value) return
|
||||
await navigator.clipboard.writeText(userDid.value)
|
||||
await safeClipboardWrite(userDid.value)
|
||||
didCopied.value = true
|
||||
setTimeout(() => { didCopied.value = false }, 2000)
|
||||
}
|
||||
@@ -2016,7 +2109,7 @@ async function showDidDocument() {
|
||||
|
||||
async function copyDidDocument() {
|
||||
if (!didDocumentFormatted.value) return
|
||||
await navigator.clipboard.writeText(didDocumentFormatted.value)
|
||||
await safeClipboardWrite(didDocumentFormatted.value)
|
||||
didDocCopied.value = true
|
||||
setTimeout(() => { didDocCopied.value = false }, 2000)
|
||||
}
|
||||
@@ -2411,7 +2504,7 @@ async function finalizePsbt() {
|
||||
|
||||
function copyPsbt() {
|
||||
if (!psbtData.value) return
|
||||
window.navigator.clipboard.writeText(psbtData.value)
|
||||
window.safeClipboardWrite(psbtData.value)
|
||||
unifiedSendError.value = t('web5.psbtCopied')
|
||||
}
|
||||
|
||||
@@ -2478,12 +2571,27 @@ async function unifiedReceive() {
|
||||
}
|
||||
|
||||
function copyEcashToken(token: string) {
|
||||
navigator.clipboard.writeText(token)
|
||||
safeClipboardWrite(token)
|
||||
showIdentityToast(t('web5.ecashTokenCopied'))
|
||||
}
|
||||
|
||||
async function safeClipboardWrite(text: string): Promise<void> {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await safeClipboardWrite(text)
|
||||
} else {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.opacity = '0'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(text: string, msg: string) {
|
||||
navigator.clipboard.writeText(text)
|
||||
safeClipboardWrite(text)
|
||||
showIdentityToast(msg)
|
||||
}
|
||||
|
||||
@@ -2711,7 +2819,7 @@ function downloadPeerContent(item: PeerContentItem) {
|
||||
if (!browsePeerOnion.value) return
|
||||
const url = `http://${browsePeerOnion.value}/content/${item.id}`
|
||||
showIdentityToast(t('web5.downloadUrlCopied'))
|
||||
navigator.clipboard.writeText(url)
|
||||
safeClipboardWrite(url)
|
||||
}
|
||||
|
||||
function closePlayer() {
|
||||
@@ -2743,7 +2851,7 @@ function onPlayerError() {
|
||||
|
||||
function copyStreamUrl() {
|
||||
if (streamUrl.value) {
|
||||
navigator.clipboard.writeText(streamUrl.value)
|
||||
safeClipboardWrite(streamUrl.value)
|
||||
showIdentityToast(t('web5.streamUrlCopied'))
|
||||
}
|
||||
}
|
||||
@@ -2832,8 +2940,8 @@ const settingVisibility = ref(false)
|
||||
|
||||
const visibilityOptions = [
|
||||
{ value: 'hidden' as VisibilityLevel, label: 'Hidden', description: 'Your node is not discoverable by others' },
|
||||
{ value: 'discoverable' as VisibilityLevel, label: 'Discoverable', description: 'Other Archipelago nodes can find you via Nostr' },
|
||||
{ value: 'public' as VisibilityLevel, label: 'Public', description: 'Visible to everyone with your onion address published' },
|
||||
{ value: 'discoverable' as VisibilityLevel, label: 'Discoverable', description: 'Federated peers can find and connect to your node' },
|
||||
{ value: 'public' as VisibilityLevel, label: 'Public', description: 'Accepting connections from any Archipelago node' },
|
||||
]
|
||||
|
||||
async function loadVisibility() {
|
||||
@@ -2869,7 +2977,7 @@ async function setVisibility(level: VisibilityLevel) {
|
||||
|
||||
function copyOnionAddress() {
|
||||
if (!nodeOnionAddress.value) return
|
||||
navigator.clipboard.writeText(nodeOnionAddress.value)
|
||||
safeClipboardWrite(nodeOnionAddress.value)
|
||||
showIdentityToast(t('web5.onionAddressCopied'))
|
||||
}
|
||||
|
||||
@@ -2928,7 +3036,7 @@ async function createIdentity() {
|
||||
}
|
||||
|
||||
function copyIdentityDid(did: string) {
|
||||
navigator.clipboard.writeText(did)
|
||||
safeClipboardWrite(did)
|
||||
showIdentityToast(t('web5.didCopied'))
|
||||
}
|
||||
|
||||
@@ -3098,8 +3206,7 @@ async function connectWallet() {
|
||||
}
|
||||
|
||||
function manageRelays() {
|
||||
// TODO: Navigate to relay management or open modal
|
||||
if (import.meta.env.DEV) console.log('Managing Nostr relays...')
|
||||
showRelaysModal.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</Transition>
|
||||
|
||||
<!-- Open Channel Modal -->
|
||||
<div v-if="showOpenModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showOpenModal = false">
|
||||
<div v-if="showOpenModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/10 backdrop-blur-md" @click.self="showOpenModal = false">
|
||||
<div class="glass-card p-6 w-full max-w-md mx-4">
|
||||
<h2 class="text-lg font-bold text-white mb-4">Open Channel</h2>
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Close Confirmation Modal -->
|
||||
<div v-if="closeTarget" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="closeTarget = null">
|
||||
<div v-if="closeTarget" class="fixed inset-0 z-50 flex items-center justify-center bg-black/10 backdrop-blur-md" @click.self="closeTarget = null">
|
||||
<div class="glass-card p-6 w-full max-w-sm mx-4">
|
||||
<h2 class="text-lg font-bold text-white mb-2">Close Channel?</h2>
|
||||
<p class="text-white/60 text-sm mb-4">This will cooperatively close the channel with peer {{ closeTarget.remote_pubkey.slice(0, 16) }}...</p>
|
||||
|
||||
Reference in New Issue
Block a user