learning_ai_clock/ios/ChronoMind/Shared/Sharing/ShareableTimerManager.swift

178 lines
5.4 KiB
Swift

// Shareable Timer Manager
// Create shareable timer links: chronom.ind/t/abc123
// iOS Universal Links open app directly
// Web fallback PWA timer page
import Foundation
import Combine
@MainActor
final class ShareableTimerManager: ObservableObject {
static let shared = ShareableTimerManager()
@Published var sharedTimers: [SharedTimer] = []
private let storageKey = "chronomind-shared-timers"
private let baseURL = "https://chronomind.app/t/"
private init() {
loadSharedTimers()
}
// MARK: - Create Shareable Link
func createShareableLink(for timer: CMTimer) -> SharedTimer {
let shareCode = generateShareCode()
let shared = SharedTimer(
code: shareCode,
label: timer.label,
type: timer.type,
durationSeconds: timer.duration,
urgency: timer.urgency,
cascadePreset: timer.cascade.preset,
category: timer.category,
createdBy: nil, // anonymized
createdAt: Date(),
expiresAt: Calendar.current.date(byAdding: .day, value: 30, to: Date())!
)
sharedTimers.append(shared)
saveSharedTimers()
return shared
}
/// Full shareable URL
func shareURL(for shared: SharedTimer) -> URL {
URL(string: "\(baseURL)\(shared.code)")!
}
/// Share text for messaging
func shareText(for shared: SharedTimer) -> String {
let url = shareURL(for: shared)
return "Check out my \(shared.label) timer on ChronoMind: \(url.absoluteString)"
}
// MARK: - Import from Link
/// Parse a share link and create a timer from it
func importFromURL(_ url: URL) -> CMTimer? {
guard let code = extractShareCode(from: url) else { return nil }
return importFromCode(code)
}
func importFromCode(_ code: String) -> CMTimer? {
// Look up in local shared timers first
guard let shared = sharedTimers.first(where: { $0.code == code }) else {
// In production, fetch from server
return nil
}
guard !shared.isExpired else { return nil }
// Create timer from shared data
switch shared.type {
case .countdown:
return createCountdown(CreateCountdownParams(
label: shared.label,
durationSeconds: shared.durationSeconds,
urgency: shared.urgency,
cascade: CascadeConfig(preset: shared.cascadePreset, intervals: []),
category: shared.category
))
case .alarm:
// Alarm needs a target time set to duration from now
return createAlarm(CreateAlarmParams(
label: shared.label,
targetTime: Date().addingTimeInterval(shared.durationSeconds),
urgency: shared.urgency,
cascade: CascadeConfig(preset: shared.cascadePreset, intervals: [])
))
case .pomodoro:
let workMinutes = Int(shared.durationSeconds / 60)
return createPomodoro(CreatePomodoroParams(
label: shared.label,
config: PomodoroConfig(
workMinutes: workMinutes,
breakMinutes: 5,
longBreakMinutes: 15,
rounds: 4
)
))
}
}
// MARK: - URL Handling
func extractShareCode(from url: URL) -> String? {
let path = url.path
// Handle: chronom.ind/t/abc123 or chronomind.app/t/abc123
if path.hasPrefix("/t/") {
return String(path.dropFirst(3))
}
return nil
}
func canHandleURL(_ url: URL) -> Bool {
let host = url.host ?? ""
return (host == "chronomind.app" || host == "chronom.ind") && url.path.hasPrefix("/t/")
}
// MARK: - Manage Shared Timers
func revokeShare(_ code: String) {
sharedTimers.removeAll { $0.code == code }
saveSharedTimers()
}
func cleanExpired() {
sharedTimers.removeAll { $0.isExpired }
saveSharedTimers()
}
// MARK: - Private
private func generateShareCode() -> String {
// 8-character alphanumeric code
let chars = "abcdefghijklmnopqrstuvwxyz0123456789"
return String((0..<8).map { _ in chars.randomElement()! })
}
private func loadSharedTimers() {
guard let data = UserDefaults.standard.data(forKey: storageKey),
let decoded = try? JSONDecoder().decode([SharedTimer].self, from: data) else { return }
sharedTimers = decoded
}
private func saveSharedTimers() {
if let data = try? JSONEncoder().encode(sharedTimers) {
UserDefaults.standard.set(data, forKey: storageKey)
}
}
}
// MARK: - Shared Timer Model
struct SharedTimer: Codable, Identifiable {
let code: String
let label: String
let type: CMTimerType
let durationSeconds: TimeInterval
let urgency: UrgencyLevel
let cascadePreset: CascadePreset
let category: String?
let createdBy: String?
let createdAt: Date
let expiresAt: Date
var id: String { code }
var isExpired: Bool {
Date() > expiresAt
}
var shareURL: URL {
URL(string: "https://chronomind.app/t/\(code)")!
}
}