Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions toss/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>lutra-labs.toss</string>
<key>CFBundleURLSchemes</key>
<array>
<string>tossinger</string>
</array>
</dict>
</array>
<key>NSAccessibilityUsageDescription</key>
<string>Tossinger needs Accessibility access to capture selected text from other apps when you use the global shortcut (experimental).</string>
<key>UIBackgroundModes</key>
Expand Down
9 changes: 9 additions & 0 deletions toss/Services/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import TossKit

@MainActor
class AppSettings: ObservableObject {
// Shared with the iOS share extension via the app group so both processes
// see the same value for `autoOpenAfterSharing`. Existing single-process
// preferences below continue to live in UserDefaults.standard so they
// aren't wiped on upgrade.
static let sharedDefaults = UserDefaults(suiteName: TossPersistenceStack.appGroupIdentifier)!

@AppStorage("biometric_enabled") var isBiometricEnabled: Bool = false

@AppStorage("layout_mode") private var layoutModeRaw: String = TossLayoutMode.grid.rawValue
Expand All @@ -19,4 +25,7 @@ class AppSettings: ObservableObject {
get { TossLayoutMode(rawValue: layoutModeRaw) ?? .grid }
set { layoutModeRaw = newValue.rawValue }
}

@AppStorage("auto_open_after_share", store: AppSettings.sharedDefaults)
var autoOpenAfterSharing: Bool = false
}
30 changes: 30 additions & 0 deletions toss/Views/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ struct SettingsView: View {
NavigationStack {
Form {
securitySection
sharingSection
aboutSection

Section {
Expand Down Expand Up @@ -422,6 +423,35 @@ struct SettingsView: View {
Text("About")
}
}

#if os(iOS)
private var sharingSection: some View {
Section {
Toggle(isOn: $appSettings.autoOpenAfterSharing) {
HStack(spacing: 12) {
Image(systemName: "square.and.arrow.up")
.foregroundStyle(.blue)
.font(.title3)
.frame(width: 24)

VStack(alignment: .leading, spacing: 2) {
Text("Auto-open app after sharing")
.font(.body)
Text("Ensures tosses sync to iCloud immediately")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
} header: {
Text("Sharing")
} footer: {
Text(
"When off, tosses shared from other apps are saved locally and only reach iCloud the next time you open Tossinger. Turn on to briefly open Tossinger after each share so changes sync immediately."
)
}
}
#endif
}

#Preview {
Expand Down
7 changes: 7 additions & 0 deletions toss/tossApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ struct tossApp: App {
break
}
}
#if os(iOS)
.onOpenURL { _ in
// Intentional no-op. The share extension opens `tossinger://share-complete`
// purely to foreground Tossinger so NSPersistentCloudKitContainer can
// complete its pending CloudKit export. The URL itself carries no data.
}
#endif
}
.modelContainer(container)
#if os(macOS)
Expand Down
42 changes: 38 additions & 4 deletions tossShare/ShareViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ final class ShareViewController: UIViewController {
private let successLabel = UILabel()
private var didCompleteRequest = false

/// User-opt-in flag read from the app-group UserDefaults, written by the
/// main app's Settings screen. When true, the extension foregrounds the
/// main Tossinger app after saving so NSPersistentCloudKitContainer can
/// complete its pending CloudKit export (extensions cannot finish the
/// export themselves from a short-lived process).
private let autoOpenAfterSharing: Bool = {
UserDefaults(suiteName: TossPersistenceStack.appGroupIdentifier)?
.bool(forKey: "auto_open_after_share") ?? false
}()

private var successMessage: String {
autoOpenAfterSharing
? "Tossed. Opening app…"
: "Tossed. Open app to sync."
}

private var successCloseDelay: TimeInterval {
autoOpenAfterSharing ? 0.35 : 0.8
}

override func viewDidLoad() {
super.viewDidLoad()
setupUI()
Expand All @@ -44,7 +64,7 @@ final class ShareViewController: UIViewController {
view.backgroundColor = .systemBackground

// Configure success label
successLabel.text = "Tossed and syncing..."
successLabel.text = successMessage
successLabel.font = .systemFont(ofSize: 17, weight: .medium)
successLabel.textAlignment = .center
successLabel.textColor = .label
Expand Down Expand Up @@ -167,7 +187,7 @@ final class ShareViewController: UIViewController {

do {
try context.save()
showMessage("Tossed and syncing...")
showMessage(successMessage)
} catch {
showMessageAndClose("Failed to save toss.")
return
Expand All @@ -189,7 +209,7 @@ final class ShareViewController: UIViewController {
// Keep the initial save; metadata enrichment is best-effort.
}

closeExtension(after: 0.35)
finishAfterSuccess()
}

private func applyMetadata(_ result: MetadataResult, to toss: Toss) {
Expand Down Expand Up @@ -262,7 +282,21 @@ final class ShareViewController: UIViewController {
}

private func showSuccessAndClose() {
showMessageAndClose("Tossed and syncing...")
showMessage(successMessage)
finishAfterSuccess()
}

/// Close path for a successful save. If the user opted into auto-open, the
/// extension foregrounds the containing app via its custom URL scheme so
/// NSPersistentCloudKitContainer can complete its CloudKit export. The
/// extension always tears itself down afterwards — `completeRequest` is
/// called unconditionally via `closeExtension(after:)` so the extension
/// exits even if `open` was denied by the system.
private func finishAfterSuccess() {
if autoOpenAfterSharing, let url = URL(string: "tossinger://share-complete") {
extensionContext?.open(url, completionHandler: nil)
}
closeExtension(after: successCloseDelay)
}

private func closeExtension(after delay: TimeInterval = 0) {
Expand Down
Loading