montana/macOS/MontanaPresence/WalletTabView.swift

537 lines
25 KiB
Swift
Raw Normal View History

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)
}
}
}