montana/macOS/MontanaPresence/MenuBarView.swift

550 lines
26 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 MenuBarView: View {
@EnvironmentObject var engine: PresenceEngine
@EnvironmentObject var updater: UpdateManager
@State private var showSend = false
@State private var showReceive = false
@State private var showExplorer = false
@State private var showHistory = false
@State private var showNetworkNodes = false
@State private var showTimeChainWindow = 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 {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
// 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)
}
}
// ИСТОРИЯ / ЦЕПОЧКА ВРЕМЕНИ
HStack(spacing: 8) {
Button(action: { showHistory = true }) {
HStack(spacing: 5) {
Image(systemName: "clock.arrow.circlepath")
.font(.system(size: 12, weight: .bold))
Text("\u{0418}\u{0441}\u{0442}\u{043e}\u{0440}\u{0438}\u{044f}")
.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: $showHistory) {
HistoryView().environmentObject(engine)
}
Button(action: {
showTimeChainWindow = true
}) {
HStack(spacing: 5) {
Image(systemName: "pentagon")
.font(.system(size: 13, weight: .bold))
Text("\u{0426}\u{0435}\u{043f}\u{043e}\u{0447}\u{043a}\u{0430}")
.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)
}
}
.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)
Text("v\(engine.protocolVersion)")
.font(.system(size: 9, design: .monospaced))
.foregroundColor(Color.white.opacity(0.25))
}
}
.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)
HStack {
Button(action: {
NSApp.activate(ignoringOtherApps: true)
if #available(macOS 14.0, *) {
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
} else {
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
}) {
Text("\u{041d}\u{0430}\u{0441}\u{0442}\u{0440}\u{043e}\u{0439}\u{043a}\u{0438}")
.font(.system(size: 11))
.foregroundColor(Color.white.opacity(0.4))
}
.buttonStyle(.plain)
.keyboardShortcut(",", modifiers: .command)
Spacer()
Text("v\(appVersion)")
.font(.system(size: 9, design: .monospaced))
.foregroundColor(goldDim.opacity(0.6))
Spacer()
Button(action: {
NSApplication.shared.terminate(nil)
}) {
Text("\u{0412}\u{044b}\u{0445}\u{043e}\u{0434}")
.font(.system(size: 11))
.foregroundColor(Color.white.opacity(0.3))
}
.buttonStyle(.plain)
.keyboardShortcut("q", modifiers: .command)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
// VERSION FOOTER
HStack {
Text("Montana \u{0248} v\(appVersion)")
.font(.system(size: 9, design: .monospaced))
.foregroundColor(Color.white.opacity(0.15))
Spacer()
Button(action: {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString("@junomoneta", forType: .string)
}) {
Text("@junomoneta")
.font(.system(size: 9, design: .monospaced))
.foregroundColor(goldDim.opacity(0.4))
}
.buttonStyle(.plain)
.help("\u{041a}\u{043e}\u{043f}\u{0438}\u{0440}\u{043e}\u{0432}\u{0430}\u{0442}\u{044c} \u{0430}\u{043b}\u{0438}\u{0430}\u{0441}")
Spacer()
Text("build \(appBuild)")
.font(.system(size: 9, design: .monospaced))
.foregroundColor(Color.white.opacity(0.1))
}
.padding(.horizontal, 16)
.padding(.bottom, 8)
}
}
.frame(width: 320, height: 720)
.background(bg)
.onAppear {
engine.updateT2()
Task { await engine.syncBalance() }
}
}
// Version
private var appVersion: String {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "2.21.0"
}
private var appBuild: String {
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "39"
}
// 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)
}
}
}