/* * Atoll (DynamicIsland) * Copyright (C) 2024-2026 Atoll Contributors * * Originally from boring.notch project * Modified and adapted for Atoll (DynamicIsland) * See NOTICE for details. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import AppKit import Combine import Foundation extension Notification.Name { static let sharingDidFinish = Notification.Name("com.ebullioscopic.sharingDidFinish") } @MainActor final class SharingStateManager: ObservableObject { static let shared = SharingStateManager() private var activeSessions: Int = 0 { didSet { let newValue = activeSessions > 0 if newValue != preventNotchClose { preventNotchClose = newValue if !newValue { NotificationCenter.default.post(name: .sharingDidFinish, object: nil) } } } } @Published var preventNotchClose: Bool = false private var activeDelegates: [UUID: SharingLifecycleDelegate] = [:] private init() {} func requestCloseIfReady() { if !preventNotchClose { NotificationCenter.default.post(name: .sharingDidFinish, object: nil) } } func beginInteraction() { activeSessions += 1 } func endInteraction() { if activeSessions > 0 { activeSessions -= 1 } } func makeDelegate(onEnd: (() -> Void)? = nil) -> SharingLifecycleDelegate { let id = UUID() let delegate = SharingLifecycleDelegate(id: id, onEnd: { [weak self] in onEnd?() self?.unregisterDelegate(id: id) }, onBegin: { [weak self] in self?.beginInteraction() }, onFinish: { [weak self] in self?.endInteraction() }) activeDelegates[id] = delegate return delegate } private func unregisterDelegate(id: UUID) { activeDelegates.removeValue(forKey: id) } } final class SharingLifecycleDelegate: NSObject, NSSharingServiceDelegate, NSSharingServicePickerDelegate { let id: UUID private let onEnd: () -> Void private let onBegin: () -> Void private let onFinish: () -> Void private var pickerActive = false private var serviceInProgress = false private var finished = false private var timeoutTask: Task? init(id: UUID, onEnd: @escaping () -> Void, onBegin: @escaping () -> Void, onFinish: @escaping () -> Void) { self.id = id self.onEnd = onEnd self.onBegin = onBegin self.onFinish = onFinish } deinit { timeoutTask?.cancel() } func markPickerBegan() { guard !pickerActive else { return } pickerActive = true onBegin() } func markServiceBegan() { guard !serviceInProgress else { return } serviceInProgress = true onBegin() startTimeoutFallback() } private func startTimeoutFallback() { timeoutTask?.cancel() timeoutTask = Task { @MainActor [weak self] in try? await Task.sleep(for: .seconds(2)) guard let self = self, !Task.isCancelled else { return } if !self.finished { self.finishIfNeeded() } } } private func finishIfNeeded() { guard !finished else { return } finished = true timeoutTask?.cancel() onFinish() onEnd() } // MARK: - NSSharingServicePickerDelegate func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) { if service == nil { if pickerActive && !serviceInProgress { finishIfNeeded() } return } service?.delegate = self serviceInProgress = true startTimeoutFallback() } // MARK: - NSSharingServiceDelegate func sharingService(_ sharingService: NSSharingService, willShareItems items: [Any]) { if !pickerActive && !serviceInProgress { onBegin() } serviceInProgress = true } func sharingService(_ sharingService: NSSharingService, didShareItems items: [Any]) { finishIfNeeded() } func sharingService(_ sharingService: NSSharingService, didFailToShareItems items: [Any], error: Error) { finishIfNeeded() } }