montana/macOS/MontanaPresence/SharedSidebar.swift

455 lines
18 KiB
Swift
Raw Permalink Normal View History

import SwiftUI
/// Shared Sidebar (Burger Menu) for all Montana views
/// Clean release: only implemented & working features
struct SharedSidebar: View {
@Binding var isVisible: Bool
@ObservedObject private var lang = LanguageManager.shared
@ObservedObject private var sessionStore = ChatSessionStore.shared
@State private var showLogoutConfirm = false
@State private var chatsExpanded = true
var body: some View {
VStack(alignment: .leading, spacing: 0) {
sidebarHeader
Divider()
ScrollView {
VStack(alignment: .leading, spacing: 24) {
junonaSection
Divider()
montanaSection
Divider()
exchangeSection
Divider()
settingsSection
Divider()
logoutSection
}
.padding()
}
}
.frame(width: 280)
.background(Color(NSColor.controlBackgroundColor).opacity(0.95))
.shadow(radius: 10)
.alert(lang.isRu ? "\u{0412}\u{044B}\u{0439}\u{0442}\u{0438} \u{0438}\u{0437} Montana?" : "Log out of Montana?", isPresented: $showLogoutConfirm) {
Button(lang.isRu ? "\u{0412}\u{044B}\u{0439}\u{0442}\u{0438}" : "Log Out", role: .destructive) {
withAnimation { isVisible = false }
CryptoManager.shared.deleteIdentity()
}
Button(lang.isRu ? "\u{041E}\u{0442}\u{043C}\u{0435}\u{043D}\u{0430}" : "Cancel", role: .cancel) {}
} message: {
Text(lang.isRu
? "\u{041A}\u{043B}\u{044E}\u{0447}\u{0438} \u{0431}\u{0443}\u{0434}\u{0443}\u{0442} \u{0443}\u{0434}\u{0430}\u{043B}\u{0435}\u{043D}\u{044B} \u{0438}\u{0437} Keychain. \u{0423}\u{0431}\u{0435}\u{0434}\u{0438}\u{0442}\u{0435}\u{0441}\u{044C}, \u{0447}\u{0442}\u{043E} \u{0432}\u{044B} \u{0441}\u{043E}\u{0445}\u{0440}\u{0430}\u{043D}\u{0438}\u{043B}\u{0438} seed-\u{0444}\u{0440}\u{0430}\u{0437}\u{0443} \u{0434}\u{043B}\u{044F} \u{0432}\u{043E}\u{0441}\u{0441}\u{0442}\u{0430}\u{043D}\u{043E}\u{0432}\u{043B}\u{0435}\u{043D}\u{0438}\u{044F}."
: "Keys will be deleted from Keychain. Make sure you saved your seed phrase for recovery.")
}
}
// MARK: - Header
private var sidebarHeader: some View {
HStack {
if let logoPath = Bundle.main.path(forResource: "JunonaLogo", ofType: "jpg"),
let nsImage = NSImage(contentsOfFile: logoPath) {
Image(nsImage: nsImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 32, height: 32)
.clipShape(Circle())
.overlay(
Circle()
.stroke(
LinearGradient(
colors: [
Color(red: 0.83, green: 0.69, blue: 0.22),
Color(red: 0.94, green: 0.82, blue: 0.38)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1.5
)
)
} else {
Circle()
.fill(
LinearGradient(
colors: [
Color(red: 0.0, green: 0.83, blue: 1.0),
Color(red: 0.48, green: 0.18, blue: 1.0)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 32, height: 32)
.overlay(
Text("\u{042E}")
.font(.system(size: 16, weight: .bold))
.foregroundColor(.white)
)
}
VStack(alignment: .leading, spacing: 2) {
Text("Montana Protocol")
.font(.headline)
Text("\u{0248}")
.font(.caption2)
.foregroundColor(.secondary)
}
Spacer()
Button(action: {
withAnimation { isVisible = false }
}) {
Image(systemName: "xmark")
.font(.system(size: 14))
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
}
.padding()
}
// MARK: - Junona Section (with collapsible chats)
private var junonaSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Junona AI")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
// New chat button
Button(action: {
_ = sessionStore.createNewSession()
NotificationCenter.default.post(name: .switchToTab, object: nil, userInfo: ["tab": 0])
withAnimation { isVisible = false }
}) {
HStack(spacing: 8) {
Image(systemName: "plus.message")
.font(.system(size: 14))
.foregroundColor(Color(red: 0.0, green: 0.83, blue: 1.0))
.frame(width: 20)
Text(lang.isRu ? "\u{041D}\u{043E}\u{0432}\u{044B}\u{0439} \u{0447}\u{0430}\u{0442}" : "New Chat")
.font(.callout)
.foregroundColor(.primary)
Spacer()
}
.padding(.vertical, 6)
.padding(.horizontal, 12)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.onHover { h in if h { NSCursor.pointingHand.push() } else { NSCursor.pop() } }
// Collapsible saved chats
if !sessionStore.sessions.isEmpty {
DisclosureGroup(isExpanded: $chatsExpanded) {
VStack(spacing: 2) {
ForEach(sessionStore.sessions.prefix(15)) { session in
Button(action: {
sessionStore.selectSession(session.id)
NotificationCenter.default.post(name: .switchToTab, object: nil, userInfo: ["tab": 0])
withAnimation { isVisible = false }
}) {
HStack(spacing: 8) {
Image(systemName: session.id == sessionStore.currentSessionId ? "message.fill" : "message")
.font(.system(size: 11))
.foregroundColor(session.id == sessionStore.currentSessionId
? Color(red: 0.83, green: 0.69, blue: 0.22)
: .secondary)
.frame(width: 16)
Text(session.title)
.font(.caption)
.foregroundColor(session.id == sessionStore.currentSessionId ? .primary : .secondary)
.lineLimit(1)
Spacer()
}
.padding(.vertical, 4)
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 5)
.fill(session.id == sessionStore.currentSessionId
? Color(red: 0.83, green: 0.69, blue: 0.22).opacity(0.1)
: Color.clear)
)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.onHover { h in if h { NSCursor.pointingHand.push() } else { NSCursor.pop() } }
.contextMenu {
Button(role: .destructive) {
sessionStore.deleteSession(session.id)
} label: {
Label(lang.isRu ? "\u{0423}\u{0434}\u{0430}\u{043B}\u{0438}\u{0442}\u{044C}" : "Delete", systemImage: "trash")
}
}
}
}
} label: {
HStack(spacing: 6) {
Image(systemName: "bubble.left.and.bubble.right")
.font(.system(size: 11))
Text(lang.isRu ? "\u{0427}\u{0430}\u{0442}\u{044B} (\(sessionStore.sessions.count))" : "Chats (\(sessionStore.sessions.count))")
.font(.caption)
}
.foregroundColor(.secondary)
.padding(.horizontal, 12)
}
}
}
}
// MARK: - Montana Section (only working features)
private var montanaSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Montana")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
VStack(spacing: 4) {
SidebarNavItem(icon: "banknote", label: lang.isRu ? "\u{041A}\u{043E}\u{0448}\u{0435}\u{043B}\u{0451}\u{043A}" : "Wallet", tag: 1, isVisible: $isVisible)
SidebarNavItem(icon: "pentagon", label: lang.isRu ? "\u{0422}\u{0430}\u{0439}\u{043C}\u{0447}\u{0435}\u{0439}\u{043D}" : "TimeChain", tag: 8, isVisible: $isVisible)
SidebarNavItem(icon: "clock.arrow.circlepath", label: lang.isRu ? "\u{0418}\u{0441}\u{0442}\u{043E}\u{0440}\u{0438}\u{044F}" : "History", tag: 7, isVisible: $isVisible)
}
}
}
// MARK: - Exchange Section
private var exchangeSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text(lang.isRu ? "\u{041E}\u{0431}\u{043C}\u{0435}\u{043D}" : "Exchange")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
VStack(spacing: 4) {
SidebarExchangeItem(icon: "bitcoinsign.circle.fill", label: "BTC \u{2192} \u{0248}")
SidebarExchangeItem(icon: "dollarsign.circle.fill", label: "USD \u{2192} \u{0248}")
SidebarExchangeItem(icon: "rublesign.circle.fill", label: "RUB \u{2192} \u{0248}")
}
}
}
// MARK: - Settings
private var settingsSection: some View {
SidebarNavItem(icon: "gear", label: lang.isRu ? "\u{041D}\u{0430}\u{0441}\u{0442}\u{0440}\u{043E}\u{0439}\u{043A}\u{0438}" : "Settings", tag: 9, isVisible: $isVisible)
}
// MARK: - Logout
private var logoutSection: some View {
Button(action: { showLogoutConfirm = true }) {
HStack(spacing: 8) {
Image(systemName: "rectangle.portrait.and.arrow.right")
.font(.system(size: 14))
.foregroundColor(.red.opacity(0.8))
.frame(width: 20)
Text(lang.isRu ? "\u{0412}\u{044B}\u{0445}\u{043E}\u{0434}" : "Log Out")
.font(.callout)
.foregroundColor(.red.opacity(0.8))
Spacer()
}
.padding(.vertical, 6)
.padding(.horizontal, 12)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.onHover { h in if h { NSCursor.pointingHand.push() } else { NSCursor.pop() } }
}
}
// MARK: - Nav Item
struct SidebarNavItem: View {
let icon: String
let label: String
let tag: Int
@Binding var isVisible: Bool
var body: some View {
Button(action: {
NotificationCenter.default.post(name: .switchToTab, object: nil, userInfo: ["tab": tag])
withAnimation { isVisible = false }
}) {
HStack(spacing: 8) {
Image(systemName: icon)
.font(.system(size: 14))
.foregroundColor(Color(red: 0.0, green: 0.83, blue: 1.0))
.frame(width: 20)
Text(label)
.font(.callout)
.foregroundColor(.primary)
Spacer()
}
.padding(.vertical, 6)
.padding(.horizontal, 12)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.onHover { h in if h { NSCursor.pointingHand.push() } else { NSCursor.pop() } }
}
}
// MARK: - Exchange Item
struct SidebarExchangeItem: View {
let icon: String
let label: String
@State private var showExchange = false
var currency: String {
if label.contains("BTC") { return "BTC" }
if label.contains("USD") { return "USD" }
if label.contains("RUB") { return "RUB" }
return "USD"
}
var body: some View {
Button(action: { showExchange = true }) {
HStack(spacing: 8) {
Image(systemName: icon)
.font(.system(size: 14))
.foregroundColor(Color(red: 0.0, green: 0.83, blue: 1.0))
.frame(width: 20)
Text(label)
.font(.callout)
.foregroundColor(.primary)
Spacer()
}
.padding(.vertical, 6)
.padding(.horizontal, 12)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.onHover { h in if h { NSCursor.pointingHand.push() } else { NSCursor.pop() } }
.popover(isPresented: $showExchange) {
ExchangeCalculatorView(currency: currency)
}
}
}
// MARK: - Exchange Calculator
struct ExchangeCalculatorView: View {
let currency: String
@ObservedObject private var lang = LanguageManager.shared
@State private var amount: String = ""
@State private var isReversed = false
private let gold = Color(red: 0.85, green: 0.68, blue: 0.25)
private let cyan = Color(red: 0.0, green: 0.83, blue: 1.0)
private let rateUSD: Double = 0.1605
private let rateRUB: Double = 12.04
private let rateBTC: Double = 0.0000000165
private var rate: Double {
switch currency {
case "BTC": return rateBTC
case "RUB": return rateRUB
default: return rateUSD
}
}
private var currencySymbol: String {
switch currency {
case "BTC": return "BTC"
case "RUB": return "\u{20BD}"
default: return "$"
}
}
private var convertedAmount: String {
guard let value = Double(amount), value > 0 else { return "0" }
let result: Double
if isReversed { result = value * rate } else { result = value / rate }
if currency == "BTC" && !isReversed { return String(format: "%.0f", result) }
if result < 0.01 { return String(format: "%.8f", result) }
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.groupingSeparator = " "
return formatter.string(from: NSNumber(value: result)) ?? "\(result)"
}
var body: some View {
VStack(spacing: 16) {
HStack {
Image(systemName: "arrow.left.arrow.right.circle.fill")
.foregroundColor(gold)
Text(lang.isRu ? "\u{041A}\u{043E}\u{043D}\u{0432}\u{0435}\u{0440}\u{0442}\u{0435}\u{0440}" : "Converter")
.font(.system(size: 14, weight: .bold))
Spacer()
}
Text("1 \u{0248} = \(rateDisplay)")
.font(.system(size: 11))
.foregroundColor(.secondary)
VStack(spacing: 8) {
HStack {
Text(isReversed ? "\u{0248}" : currencySymbol)
.font(.system(size: 14, weight: .bold))
.foregroundColor(isReversed ? gold : cyan)
.frame(width: 30)
TextField("0", text: $amount)
.textFieldStyle(.plain)
.font(.system(size: 16, weight: .medium, design: .monospaced))
}
.padding(10)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
Button(action: { isReversed.toggle() }) {
Image(systemName: "arrow.up.arrow.down")
.font(.system(size: 12))
.foregroundColor(gold)
.padding(6)
.background(gold.opacity(0.1))
.clipShape(Circle())
}
.buttonStyle(.plain)
HStack {
Text(isReversed ? currencySymbol : "\u{0248}")
.font(.system(size: 14, weight: .bold))
.foregroundColor(isReversed ? cyan : gold)
.frame(width: 30)
Text(convertedAmount)
.font(.system(size: 16, weight: .bold, design: .monospaced))
.foregroundColor(.primary)
Spacer()
}
.padding(10)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
Text("Genesis Price (09.01.2026)")
.font(.system(size: 9))
.foregroundColor(.secondary)
}
.padding(16)
.frame(width: 260)
}
private var rateDisplay: String {
switch currency {
case "BTC": return String(format: "%.10f BTC", rateBTC)
case "RUB": return String(format: "%.2f \u{20BD}", rateRUB)
default: return String(format: "$%.4f", rateUSD)
}
}
}