178 lines
5.4 KiB
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)")!
|
|
}
|
|
}
|