Files
oai-swift/oAI/Views/Main/SyncStatusIndicator.swift
T
runeandClaude Sonnet 5 0af644456e Update commercial licensing contact URL to oai.pm
Consolidates the mac.oai.pm subdomain references (introduced in the
PolyForm Noncommercial relicense) to the root oai.pm domain, across
the LICENSE file, README, and all Swift source file headers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:34:04 +02:00

186 lines
5.2 KiB
Swift

//
// SyncStatusIndicator.swift
// oAI
//
// Git sync status indicator (bottom-right corner)
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling oAI or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://oai.pm>.
import SwiftUI
enum SyncState {
case disabled // Gray - sync not configured or disabled
case synced // Green - successfully synced
case syncing // Yellow - sync in progress
case error(String) // Red - sync failed with error message
var color: Color {
switch self {
case .disabled: return .secondary
case .synced: return .green
case .syncing: return .orange
case .error: return .red
}
}
var icon: String {
switch self {
case .disabled: return "arrow.triangle.2.circlepath"
case .synced: return "checkmark.circle.fill"
case .syncing: return "arrow.triangle.2.circlepath"
case .error: return "exclamationmark.triangle.fill"
}
}
var tooltipText: String {
switch self {
case .disabled:
return "Auto-sync disabled"
case .synced:
if let lastSync = GitSyncService.shared.syncStatus.lastSyncTime {
return "Last synced: \(timeAgo(lastSync))"
} else {
return "Synced"
}
case .syncing:
return "Syncing..."
case .error(let message):
return "Sync failed: \(message)"
}
}
private func timeAgo(_ date: Date) -> String {
let seconds = Int(Date().timeIntervalSince(date))
if seconds < 60 {
return "just now"
} else if seconds < 3600 {
let minutes = seconds / 60
return "\(minutes)m ago"
} else if seconds < 86400 {
let hours = seconds / 3600
return "\(hours)h ago"
} else {
let days = seconds / 86400
return "\(days)d ago"
}
}
}
struct SyncStatusIndicator: View {
@State private var isHovering = false
@State private var syncState: SyncState = .disabled
@State private var showSettings = false
private let settings = SettingsService.shared
private let gitSync = GitSyncService.shared
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
// Floating indicator
ZStack {
// Background circle
Circle()
.fill(Color(nsColor: .windowBackgroundColor))
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
.frame(width: 40, height: 40)
// Icon
statusIcon
}
.scaleEffect(isHovering ? 1.1 : 1.0)
.animation(.spring(response: 0.3), value: isHovering)
.onHover { hovering in
isHovering = hovering
}
.help(syncState.tooltipText)
.onTapGesture {
if case .error = syncState {
// Open settings on error
showSettings = true
}
}
.sheet(isPresented: $showSettings) {
SettingsView()
}
.padding(.trailing, 16)
.padding(.bottom, 16)
}
}
.onAppear {
updateState()
}
.onChange(of: gitSync.syncStatus) {
updateState()
}
.onChange(of: gitSync.isSyncing) {
updateState()
}
.onChange(of: gitSync.lastSyncError) {
updateState()
}
.onChange(of: settings.syncEnabled) {
updateState()
}
.onChange(of: settings.syncAutoSave) {
updateState()
}
}
private var statusIcon: some View {
Image(systemName: syncState.icon)
.font(.system(size: 18))
.foregroundStyle(syncState.color)
}
private func updateState() {
// Determine current sync state
guard settings.syncEnabled && settings.syncConfigured else {
syncState = .disabled
return
}
guard gitSync.syncStatus.isCloned else {
syncState = .disabled
return
}
// Check for error
if let error = gitSync.lastSyncError {
syncState = .error(error)
return
}
// Check if currently syncing
if gitSync.isSyncing {
syncState = .syncing
return
}
// All good
syncState = .synced
}
}
#Preview {
SyncStatusIndicator()
}