montana/macOS/MontanaPresence/WalletTabView.swift

537 lines
25 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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