455 lines
18 KiB
Swift
455 lines
18 KiB
Swift
|
|
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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|