ソースを参照

Add Settings Share App menu and More Apps App Store link.

Wire Share App to the system share picker anchored beside the row icon, and open the developer catalog when More Apps is tapped, with App Store IDs configurable via build settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 週間 前
コミット
b53e4ef8e1
共有3 個のファイルを変更した150 個の追加3 個の削除を含む
  1. 4 0
      App for Indeed.xcodeproj/project.pbxproj
  2. 74 0
      App for Indeed/AppMarketingLinks.swift
  3. 72 3
      App for Indeed/Views/DashboardView.swift

+ 4 - 0
App for Indeed.xcodeproj/project.pbxproj

@@ -255,6 +255,8 @@
255
 				ENABLE_APP_SANDBOX = YES;
255
 				ENABLE_APP_SANDBOX = YES;
256
 				ENABLE_USER_SELECTED_FILES = readonly;
256
 				ENABLE_USER_SELECTED_FILES = readonly;
257
 				GENERATE_INFOPLIST_FILE = YES;
257
 				GENERATE_INFOPLIST_FILE = YES;
258
+				INFOPLIST_KEY_AppStoreAppID = "$(APP_STORE_APP_ID)";
259
+				INFOPLIST_KEY_AppStoreDeveloperID = "$(APP_STORE_DEVELOPER_ID)";
258
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
260
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
259
 				INFOPLIST_KEY_NSMainStoryboardFile = Main;
261
 				INFOPLIST_KEY_NSMainStoryboardFile = Main;
260
 				INFOPLIST_KEY_NSPrincipalClass = NSApplication;
262
 				INFOPLIST_KEY_NSPrincipalClass = NSApplication;
@@ -289,6 +291,8 @@
289
 				ENABLE_APP_SANDBOX = YES;
291
 				ENABLE_APP_SANDBOX = YES;
290
 				ENABLE_USER_SELECTED_FILES = readonly;
292
 				ENABLE_USER_SELECTED_FILES = readonly;
291
 				GENERATE_INFOPLIST_FILE = YES;
293
 				GENERATE_INFOPLIST_FILE = YES;
294
+				INFOPLIST_KEY_AppStoreAppID = "$(APP_STORE_APP_ID)";
295
+				INFOPLIST_KEY_AppStoreDeveloperID = "$(APP_STORE_DEVELOPER_ID)";
292
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
296
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
293
 				INFOPLIST_KEY_NSMainStoryboardFile = Main;
297
 				INFOPLIST_KEY_NSMainStoryboardFile = Main;
294
 				INFOPLIST_KEY_NSPrincipalClass = NSApplication;
298
 				INFOPLIST_KEY_NSPrincipalClass = NSApplication;

+ 74 - 0
App for Indeed/AppMarketingLinks.swift

@@ -0,0 +1,74 @@
1
+//
2
+//  AppMarketingLinks.swift
3
+//  App for Indeed
4
+//
5
+
6
+import Foundation
7
+
8
+/// Mac App Store URLs for Settings → Share App / More Apps.
9
+/// Override via build settings: `INFOPLIST_KEY_AppStoreAppID` and `INFOPLIST_KEY_AppStoreDeveloperID`.
10
+enum AppMarketingLinks {
11
+    /// Numeric App Store app ID from App Store Connect (e.g. `1234567890`).
12
+    static var macAppStoreURL: URL? {
13
+        guard let appID = resolvedAppStoreAppID else { return nil }
14
+        return URL(string: "https://apps.apple.com/app/id\(appID)")
15
+    }
16
+
17
+    /// Developer catalog on the App Store (other apps by the same publisher).
18
+    static var developerAppsURL: URL? {
19
+        guard let developerID = resolvedAppStoreDeveloperID else { return nil }
20
+        return URL(string: "https://apps.apple.com/developer/-/id\(developerID)")
21
+    }
22
+
23
+    static var appDisplayName: String {
24
+        if let display = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String,
25
+           !display.isEmpty {
26
+            return display
27
+        }
28
+        if let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String,
29
+           !name.isEmpty {
30
+            return name
31
+        }
32
+        return "App for Indeed"
33
+    }
34
+
35
+    static var shareText: String {
36
+        let name = appDisplayName
37
+        if let url = macAppStoreURL {
38
+            return "Check out \(name) on the Mac App Store:\n\(url.absoluteString)"
39
+        }
40
+        return "Check out \(name) on the Mac App Store."
41
+    }
42
+
43
+    static var shareEmailSubject: String {
44
+        "Check out \(appDisplayName)"
45
+    }
46
+
47
+    /// Payload for `NSSharingServicePicker` (Mail, Messages, AirDrop, Notes, Copy Link, etc.).
48
+    static var shareItems: [Any] {
49
+        var items: [Any] = [shareText as NSString]
50
+        if let url = macAppStoreURL {
51
+            items.append(url)
52
+        }
53
+        return items
54
+    }
55
+
56
+    static var isConfigured: Bool {
57
+        macAppStoreURL != nil
58
+    }
59
+
60
+    private static var resolvedAppStoreAppID: String? {
61
+        normalizedID(from: Bundle.main.object(forInfoDictionaryKey: "AppStoreAppID"))
62
+    }
63
+
64
+    private static var resolvedAppStoreDeveloperID: String? {
65
+        normalizedID(from: Bundle.main.object(forInfoDictionaryKey: "AppStoreDeveloperID"))
66
+    }
67
+
68
+    private static func normalizedID(from value: Any?) -> String? {
69
+        guard let raw = value as? String else { return nil }
70
+        let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
71
+        guard !trimmed.isEmpty else { return nil }
72
+        return trimmed
73
+    }
74
+}

+ 72 - 3
App for Indeed/Views/DashboardView.swift

@@ -19,7 +19,7 @@ private enum PremiumSheetLayout {
19
     static let overscanExtraTop: CGFloat = 0.5
19
     static let overscanExtraTop: CGFloat = 0.5
20
 }
20
 }
21
 
21
 
22
-final class DashboardView: NSView, NSTextFieldDelegate {
22
+final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDelegate, NSSharingServiceDelegate {
23
     /// Indeed.com-inspired neutrals and brand blue (white surfaces, `#2557a7` accent, `#2d2d2d` / `#767676` text, `#d4d2d0` borders).
23
     /// Indeed.com-inspired neutrals and brand blue (white surfaces, `#2557a7` accent, `#2d2d2d` / `#767676` text, `#d4d2d0` borders).
24
     private enum Theme {
24
     private enum Theme {
25
         static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
25
         static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
@@ -161,6 +161,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
161
     private weak var sidebarUpgradeDescription: NSTextField?
161
     private weak var sidebarUpgradeDescription: NSTextField?
162
     private weak var sidebarUpgradeButton: HoverableButton?
162
     private weak var sidebarUpgradeButton: HoverableButton?
163
     private var subscriptionObserver: NSObjectProtocol?
163
     private var subscriptionObserver: NSObjectProtocol?
164
+    /// Retains the system share picker until the user picks a destination or dismisses the menu.
165
+    private var appSharePicker: NSSharingServicePicker?
164
 
166
 
165
     /// Upper bound sent to the model per request (was fixed at 8 in the prompt). Clamped when calling the API.
167
     /// Upper bound sent to the model per request (was fixed at 8 in the prompt). Clamped when calling the API.
166
     private static let jobsPerSearchDefault = 15
168
     private static let jobsPerSearchDefault = 15
@@ -1496,8 +1498,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1496
         contentStack.translatesAutoresizingMaskIntoConstraints = false
1498
         contentStack.translatesAutoresizingMaskIntoConstraints = false
1497
 
1499
 
1498
         let settingsSection = makeSettingsSection(rows: [
1500
         let settingsSection = makeSettingsSection(rows: [
1499
-            makeSettingsRow(title: "Share App", systemImage: "square.and.arrow.up", accessory: nil),
1500
-            makeSettingsRow(title: "More Apps", systemImage: "square.grid.2x2", accessory: nil)
1501
+            makeSettingsRow(title: "Share App", systemImage: "square.and.arrow.up", accessory: nil, tapAction: #selector(didTapShareApp)),
1502
+            makeSettingsRow(title: "More Apps", systemImage: "square.grid.2x2", accessory: nil, tapAction: #selector(didTapMoreApps))
1501
         ])
1503
         ])
1502
 
1504
 
1503
         let aboutTitle = NSTextField(labelWithString: "About")
1505
         let aboutTitle = NSTextField(labelWithString: "About")
@@ -1796,6 +1798,73 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1796
         focusSearchField(seed: "Find jobs that require skill: ")
1798
         focusSearchField(seed: "Find jobs that require skill: ")
1797
     }
1799
     }
1798
 
1800
 
1801
+    @objc private func didTapShareApp(_ sender: NSButton) {
1802
+        presentAppShareMenu(anchoredTo: sender)
1803
+    }
1804
+
1805
+    /// Shows the macOS share menu (Mail, Messages, AirDrop, Copy Link, etc.) with the app link.
1806
+    private func presentAppShareMenu(anchoredTo sender: NSButton) {
1807
+        guard let row = sender.superview else { return }
1808
+        let items = AppMarketingLinks.shareItems
1809
+        guard !items.isEmpty else { return }
1810
+
1811
+        let picker = NSSharingServicePicker(items: items)
1812
+        picker.delegate = self
1813
+        appSharePicker = picker
1814
+
1815
+        // Match `makeSettingsRow` layout: 16pt leading inset + 38pt icon tile — anchor the
1816
+        // popover beside the share icon, not the horizontal center of the full-width row.
1817
+        let iconTileInset: CGFloat = 16
1818
+        let iconTileSize: CGFloat = 38
1819
+        let anchorRect = NSRect(
1820
+            x: iconTileInset,
1821
+            y: row.bounds.minY + 6,
1822
+            width: iconTileSize,
1823
+            height: max(row.bounds.height - 12, 1)
1824
+        )
1825
+        picker.show(relativeTo: anchorRect, of: row, preferredEdge: .minY)
1826
+    }
1827
+
1828
+    func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, delegateFor sharingService: NSSharingService) -> Any? {
1829
+        self
1830
+    }
1831
+
1832
+    func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose sharingService: NSSharingService?) {
1833
+        appSharePicker = nil
1834
+    }
1835
+
1836
+    func sharingService(_ sharingService: NSSharingService, willShareItems items: [Any]) -> [Any] {
1837
+        if sharingService == NSSharingService(named: .composeEmail) {
1838
+            sharingService.subject = AppMarketingLinks.shareEmailSubject
1839
+        }
1840
+        return items
1841
+    }
1842
+
1843
+    @objc private func didTapMoreApps() {
1844
+        guard let url = AppMarketingLinks.developerAppsURL else {
1845
+            presentAppMarketingConfigurationAlert(feature: "More Apps")
1846
+            return
1847
+        }
1848
+        NSWorkspace.shared.open(url)
1849
+    }
1850
+
1851
+    private func presentAppMarketingConfigurationAlert(feature: String) {
1852
+        let alert = NSAlert()
1853
+        alert.messageText = "\(feature) isn’t available yet"
1854
+        alert.informativeText = """
1855
+        Add your Mac App Store IDs in the target’s build settings:
1856
+        • AppStoreAppID — numeric app ID from App Store Connect
1857
+        • AppStoreDeveloperID — numeric developer ID (for your other apps page)
1858
+        """
1859
+        alert.alertStyle = .informational
1860
+        alert.addButton(withTitle: "OK")
1861
+        if let window {
1862
+            alert.beginSheetModal(for: window)
1863
+        } else {
1864
+            alert.runModal()
1865
+        }
1866
+    }
1867
+
1799
     @objc private func didTapSupport() {
1868
     @objc private func didTapSupport() {
1800
         presentSettingsPlaceholderAlert(for: "Support")
1869
         presentSettingsPlaceholderAlert(for: "Support")
1801
     }
1870
     }