import SwiftUI import AppKit struct WalletTabView: View { @EnvironmentObject var engine: PresenceEngine @EnvironmentObject var updater: UpdateManager @EnvironmentObject var vpn: VPNManager @State private var showSend = false @State private var showReceive = false @State private var showNetworkNodes = false @State private var coinRotation: Double = 0 @State private var showSidebar = false // Montana palette — gold coin aesthetic private let gold = Color(red: 0.85, green: 0.68, blue: 0.25) private let goldLight = Color(red: 0.95, green: 0.82, blue: 0.45) private let goldDim = Color(red: 0.6, green: 0.48, blue: 0.18) private let bg = Color(red: 0.06, green: 0.06, blue: 0.08) private let cardBg = Color(red: 0.09, green: 0.09, blue: 0.12) private let dividerColor = Color(red: 0.15, green: 0.14, blue: 0.12) var body: some View { ZStack(alignment: .leading) { // Main content ScrollView { VStack(alignment: .leading, spacing: 0) { // ── BURGER MENU BUTTON ── HStack { Button(action: { withAnimation(.easeInOut(duration: 0.3)) { showSidebar = true } }) { Image(systemName: "line.3.horizontal") .font(.system(size: 18, weight: .medium)) .foregroundColor(gold) .padding(8) } .buttonStyle(.plain) Spacer() } .padding(.horizontal, 12) .padding(.top, 8) // ── SPINNING COIN ── HStack { Spacer() ZStack { // Front side: Junona face if let junonaPath = Bundle.main.path(forResource: "JunonaLogo", ofType: "jpg"), let junonaImage = NSImage(contentsOfFile: junonaPath) { Image(nsImage: junonaImage) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 80, height: 80) .clipShape(Circle()) .overlay( Circle() .stroke( LinearGradient( colors: [gold, goldLight], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 2 ) ) .opacity(cos(coinRotation * .pi / 180) > 0 ? cos(coinRotation * .pi / 180) : 0) } // Back side: Pyramid (Network logo) if let pyramidPath = Bundle.main.path(forResource: "NetworkLogo", ofType: "png"), let pyramidImage = NSImage(contentsOfFile: pyramidPath) { Image(nsImage: pyramidImage) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 60, height: 60) .padding(10) .background( Circle() .fill(Color.black) ) .overlay( Circle() .stroke( LinearGradient( colors: [goldDim, gold], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 2 ) ) .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) .opacity(cos(coinRotation * .pi / 180) < 0 ? -cos(coinRotation * .pi / 180) : 0) } } .rotation3DEffect(.degrees(coinRotation), axis: (x: 0, y: 1, z: 0)) .shadow(color: gold.opacity(0.5), radius: 20, x: 0, y: 10) .onAppear { withAnimation(.linear(duration: 4.0).repeatForever(autoreverses: false)) { coinRotation = 360 } } Spacer() } .frame(height: 100) .padding(.top, 12) .padding(.bottom, 8) // ── BALANCE HEADER ── VStack(alignment: .leading, spacing: 4) { HStack(alignment: .firstTextBaseline) { Text("\(formatNumber(engine.displayBalance))") .font(.system(size: 34, weight: .bold, design: .rounded)) .foregroundColor(.white) Text("\u{0248}") .font(.system(size: 24, weight: .bold)) .foregroundColor(gold) Spacer() Text(formatDuration(engine.sessionSeconds)) .font(.system(size: 13, design: .monospaced)) .foregroundColor(goldDim) } HStack(spacing: 8) { Text(engine.displayAddress) .font(.system(size: 11, design: .monospaced)) .foregroundColor(Color.white.opacity(0.35)) Spacer() Text("\u{2248}$\(formatCurrency(engine.balanceUSD))") .font(.system(size: 10, design: .monospaced)) .foregroundColor(Color.white.opacity(0.3)) Text("\u{2248}\(formatCurrency(engine.balanceRUB))\u{20bd}") .font(.system(size: 10, design: .monospaced)) .foregroundColor(Color.white.opacity(0.3)) } HStack { Text("+1 \u{0248}/\u{0441}\u{0435}\u{043a}") .font(.system(size: 15, weight: .semibold)) .foregroundColor(goldLight) } } .padding(.horizontal, 16) .padding(.top, 14) .padding(.bottom, 10) sep() // ── КОШЕЛЁК ── VStack(spacing: 6) { Button(action: { Task { await engine.reportToServer(); await engine.syncBalance() } }) { row(icon: engine.walletSynced ? "checkmark.shield.fill" : "arrow.triangle.2.circlepath", iconColor: engine.walletSynced ? .green : .orange, label: "\u{041a}\u{043e}\u{0448}\u{0435}\u{043b}\u{0451}\u{043a}", value: engine.walletSynced ? "\u{0441}\u{0438}\u{043d}\u{0445}\u{0440}\u{043e}\u{043d}\u{0438}\u{0437}\u{0438}\u{0440}\u{043e}\u{0432}\u{0430}\u{043d}" : "\u{0441}\u{0438}\u{043d}\u{0445}\u{0440}\u{043e}\u{043d}\u{0438}\u{0437}\u{0430}\u{0446}\u{0438}\u{044f}...", valueColor: engine.walletSynced ? .green : .orange) } .buttonStyle(.plain) row(icon: "banknote", iconColor: gold, label: "\u{0414}\u{043e}\u{0441}\u{0442}\u{0443}\u{043f}\u{043d}\u{043e}", value: "\(formatNumber(engine.availableBalance)) \u{0248}", valueColor: gold) // ── ОТПРАВИТЬ / ПОЛУЧИТЬ ── HStack(spacing: 8) { Button(action: { showSend = true }) { HStack(spacing: 5) { Text("\u{0248}") .font(.system(size: 15, weight: .bold)) Image(systemName: "arrow.up") .font(.system(size: 11, weight: .bold)) Text("\u{041e}\u{0442}\u{043f}\u{0440}\u{0430}\u{0432}\u{0438}\u{0442}\u{044c}") .font(.system(size: 12, weight: .semibold)) } .frame(maxWidth: .infinity) .padding(.vertical, 9) .background(gold.opacity(0.15)) .foregroundColor(gold) .cornerRadius(8) .overlay(RoundedRectangle(cornerRadius: 8).stroke(gold.opacity(0.3), lineWidth: 1)) } .buttonStyle(.plain) .popover(isPresented: $showSend) { SendView().environmentObject(engine) } Button(action: { showReceive = true }) { HStack(spacing: 5) { Text("\u{0248}") .font(.system(size: 15, weight: .bold)) Image(systemName: "arrow.down") .font(.system(size: 11, weight: .bold)) Text("\u{041f}\u{043e}\u{043b}\u{0443}\u{0447}\u{0438}\u{0442}\u{044c}") .font(.system(size: 12, weight: .semibold)) } .frame(maxWidth: .infinity) .padding(.vertical, 9) .background(goldDim.opacity(0.15)) .foregroundColor(goldLight) .cornerRadius(8) .overlay(RoundedRectangle(cornerRadius: 8).stroke(goldDim.opacity(0.3), lineWidth: 1)) } .buttonStyle(.plain) .popover(isPresented: $showReceive) { ReceiveView().environmentObject(engine) } } } .padding(.horizontal, 16) .padding(.vertical, 6) sep() // ── ТАЙЧЕЙН T2 ── VStack(spacing: 4) { HStack(spacing: 6) { Image(systemName: "square") .font(.system(size: 12)) .frame(width: 18) .foregroundColor(gold) Text("\u{041e}\u{043a}\u{043d}\u{043e} #\(formatNumber(engine.t2BlockNumber))") .font(.system(size: 12, weight: .semibold)) .foregroundColor(Color.white.opacity(0.8)) Spacer() Text(formatT2Time(engine.t2SecondsRemaining)) .font(.system(size: 10, design: .monospaced)) .foregroundColor(goldDim) Image(systemName: "arrow.right") .font(.system(size: 8)) .foregroundColor(Color.white.opacity(0.15)) } // Progress bar GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 3) .fill(Color.white.opacity(0.05)) .frame(height: 6) RoundedRectangle(cornerRadius: 3) .fill( LinearGradient( colors: [goldDim, gold, goldLight], startPoint: .leading, endPoint: .trailing ) ) .frame(width: max(geo.size.width * engine.t2Progress, 2), height: 6) } } .frame(height: 6) HStack { Text("+\(formatNumber(engine.t2PendingCoins)) \u{0248}") .font(.system(size: 11, weight: .medium, design: .monospaced)) .foregroundColor(goldLight) Text("\u{043d}\u{0430}\u{0447}\u{0438}\u{0441}\u{043b}\u{0435}\u{043d}\u{043e}") .font(.system(size: 10)) .foregroundColor(Color.white.opacity(0.3)) Spacer() Text("T\u{2082} = 10 \u{043c}\u{0438}\u{043d}") .font(.system(size: 9, design: .monospaced)) .foregroundColor(Color.white.opacity(0.2)) } } .padding(.horizontal, 16) .padding(.vertical, 6) sep() // ── NETWORK NODES ── VStack(spacing: 4) { Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showNetworkNodes.toggle() } }) { HStack { Image(systemName: "network") .font(.system(size: 12)) .frame(width: 18) .foregroundColor(gold) Text("\u{0421}\u{0435}\u{0442}\u{044c} Montana") .font(.system(size: 12, weight: .semibold)) .foregroundColor(Color.white.opacity(0.8)) Spacer() Text("\(engine.networkOnline)/\(engine.networkTotal) \u{0443}\u{0437}\u{043b}\u{043e}\u{0432}") .font(.system(size: 10, design: .monospaced)) .foregroundColor(engine.networkOnline == engine.networkTotal ? .green : .orange) Image(systemName: showNetworkNodes ? "chevron.up" : "chevron.down") .font(.system(size: 8)) .foregroundColor(Color.white.opacity(0.2)) } } .buttonStyle(.plain) if showNetworkNodes { ForEach(Array(engine.networkNodes.enumerated()), id: \.offset) { _, node in HStack(spacing: 6) { Circle() .fill(node.online ? Color.green : Color.red) .frame(width: 5, height: 5) Text(node.name) .font(.system(size: 10)) .foregroundColor(Color.white.opacity(0.5)) Spacer() Text(node.online ? "\u{043e}\u{043d}\u{043b}\u{0430}\u{0439}\u{043d}" : "\u{043e}\u{0444}\u{043b}\u{0430}\u{0439}\u{043d}") .font(.system(size: 9, design: .monospaced)) .foregroundColor(node.online ? .green : .red) } .padding(.leading, 24) } .transition(.opacity) } } .padding(.horizontal, 16) .padding(.vertical, 6) sep() // ── EPOCH / GENESIS ── VStack(spacing: 3) { HStack(spacing: 6) { Image(systemName: "calendar.badge.clock") .font(.system(size: 11)) .frame(width: 18) .foregroundColor(gold) Text("\u{042d}\u{043f}\u{043e}\u{0445}\u{0430} #\(engine.epochNumber)") .font(.system(size: 11)) .foregroundColor(Color.white.opacity(0.7)) Spacer() Text("\u{0434}\u{0435}\u{043d}\u{044c} \(engine.epochDays)") .font(.system(size: 10, design: .monospaced)) .foregroundColor(Color.white.opacity(0.35)) } HStack(spacing: 6) { Image(systemName: "sparkles") .font(.system(size: 11)) .frame(width: 18) .foregroundColor(goldDim) Text("\u{0413}\u{0435}\u{043d}\u{0435}\u{0437}\u{0438}\u{0441}") .font(.system(size: 11)) .foregroundColor(Color.white.opacity(0.4)) Spacer() Text("09.01.2026") .font(.system(size: 10, design: .monospaced)) .foregroundColor(Color.white.opacity(0.3)) } HStack(spacing: 6) { Image(systemName: "dollarsign.circle") .font(.system(size: 11)) .frame(width: 18) .foregroundColor(goldDim) Text("1 \u{0248}") .font(.system(size: 11)) .foregroundColor(Color.white.opacity(0.4)) Spacer() Text("$\(String(format: "%.4f", PresenceEngine.genesisPriceUSD))") .font(.system(size: 10, design: .monospaced)) .foregroundColor(Color.white.opacity(0.3)) Text("\(String(format: "%.2f", PresenceEngine.genesisPriceRUB))\u{20bd}") .font(.system(size: 10, design: .monospaced)) .foregroundColor(Color.white.opacity(0.3)) } HStack(spacing: 6) { Image(systemName: "banknote") .font(.system(size: 11)) .frame(width: 18) .foregroundColor(goldDim) Text("\u{0413}\u{0435}\u{043d}\u{0435}\u{0437}\u{0438}\u{0441} \u{0426}\u{0435}\u{043d}\u{044b}") .font(.system(size: 11)) .foregroundColor(Color.white.opacity(0.4)) Spacer() Text(PresenceEngine.genesisSettlementDate) .font(.system(size: 10, design: .monospaced)) .foregroundColor(Color.white.opacity(0.3)) } } .padding(.horizontal, 16) .padding(.vertical, 6) sep() // ── STATUS ── VStack(spacing: 3) { HStack { Circle() .fill(engine.isOnline ? Color.green : Color.orange) .frame(width: 6, height: 6) Text(engine.isOnline ? "\u{041f}\u{043e}\u{0434}\u{043a}\u{043b}\u{044e}\u{0447}\u{0435}\u{043d}" : "\u{041e}\u{0444}\u{043b}\u{0430}\u{0439}\u{043d}") .font(.system(size: 11)) .foregroundColor(Color.white.opacity(0.4)) if engine.pendingSeconds > 0 { Spacer() Text("+\(engine.pendingSeconds) \u{043e}\u{0436}\u{0438}\u{0434}\u{0430}\u{0435}\u{0442}") .font(.system(size: 10)) .foregroundColor(.orange) } } HStack(spacing: 5) { Image(systemName: "lock.fill") .font(.system(size: 9)) .foregroundColor(goldDim) Text(engine.protocolCrypto) .font(.system(size: 9, design: .monospaced)) .foregroundColor(Color.white.opacity(0.3)) Spacer() Text(engine.protocolMode) .font(.system(size: 9, weight: .bold, design: .monospaced)) .foregroundColor(.green) } } .padding(.horizontal, 16) .padding(.vertical, 6) sep() // ── CONTROLS ── VStack(spacing: 6) { Button(action: { if engine.isTracking { engine.stopTracking() } else { engine.startTracking() } }) { HStack(spacing: 6) { Image(systemName: engine.isTracking ? "stop.circle.fill" : "play.circle.fill") .font(.system(size: 14)) Text(engine.isTracking ? "\u{0421}\u{0442}\u{043e}\u{043f}" : "\u{0421}\u{0442}\u{0430}\u{0440}\u{0442}") .font(.system(size: 14, weight: .semibold)) } .frame(maxWidth: .infinity) .padding(.vertical, 8) .background(engine.isTracking ? Color.red.opacity(0.15) : gold.opacity(0.2)) .foregroundColor(engine.isTracking ? .red : gold) .cornerRadius(8) .overlay(RoundedRectangle(cornerRadius: 8).stroke( engine.isTracking ? Color.red.opacity(0.3) : gold.opacity(0.3), lineWidth: 1)) } .buttonStyle(.plain) .disabled(engine.address == nil || engine.address?.isEmpty == true) } .padding(.horizontal, 16) .padding(.vertical, 8) Spacer().frame(height: 8) } } .background(bg) .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear { engine.updateT2() Task { await engine.syncBalance() } } // Sidebar overlay if showSidebar { Color.black.opacity(0.3) .ignoresSafeArea() .onTapGesture { withAnimation(.easeInOut(duration: 0.3)) { showSidebar = false } } SharedSidebar(isVisible: $showSidebar) .transition(.move(edge: .leading)) } } } // ── Helpers ── @ViewBuilder private func sep() -> some View { dividerColor.frame(height: 0.5) .padding(.horizontal, 12) } @ViewBuilder private func row(icon: String, iconColor: Color, label: String, value: String, valueColor: Color) -> some View { HStack(spacing: 6) { Image(systemName: icon) .font(.system(size: 12)) .frame(width: 18) .foregroundColor(iconColor) Text(label) .font(.system(size: 11)) .foregroundColor(Color.white.opacity(0.6)) Spacer() Text(value) .font(.system(size: 10, design: .monospaced)) .foregroundColor(valueColor) } } private func formatNumber(_ n: Int) -> String { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.groupingSeparator = "," return formatter.string(from: NSNumber(value: n)) ?? "\(n)" } private func formatT2Time(_ seconds: Int) -> String { let m = seconds / 60 let s = seconds % 60 return String(format: "%d:%02d", m, s) } private func formatDuration(_ seconds: Int) -> String { let h = seconds / 3600 let m = (seconds % 3600) / 60 let s = seconds % 60 return String(format: "%d:%02d:%02d", h, m, s) } private func formatCurrency(_ value: Double) -> String { if value < 1 { return String(format: "%.2f", value) } else if value < 100 { return String(format: "%.1f", value) } else { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 0 formatter.groupingSeparator = "," return formatter.string(from: NSNumber(value: value)) ?? String(format: "%.0f", value) } } }