|
|
@@ -6,6 +6,7 @@
|
|
6
|
6
|
//
|
|
7
|
7
|
|
|
8
|
8
|
import Cocoa
|
|
|
9
|
+import WebKit
|
|
9
|
10
|
|
|
10
|
11
|
private enum SidebarPage: Int {
|
|
11
|
12
|
case joinMeetings = 0
|
|
|
@@ -57,6 +58,8 @@ final class ViewController: NSViewController {
|
|
57
|
58
|
private var paywallPlanViews: [PremiumPlan: NSView] = [:]
|
|
58
|
59
|
private var premiumPlanByView = [ObjectIdentifier: PremiumPlan]()
|
|
59
|
60
|
private weak var paywallOfferLabel: NSTextField?
|
|
|
61
|
+ private weak var meetLinkField: NSTextField?
|
|
|
62
|
+ private var inAppBrowserWindowController: InAppBrowserWindowController?
|
|
60
|
63
|
|
|
61
|
64
|
private let darkModeDefaultsKey = "settings.darkModeEnabled"
|
|
62
|
65
|
private var darkModeEnabled: Bool {
|
|
|
@@ -179,6 +182,65 @@ private extension ViewController {
|
|
179
|
182
|
showPaywall()
|
|
180
|
183
|
}
|
|
181
|
184
|
|
|
|
185
|
+ @objc private func joinMeetClicked(_ sender: Any?) {
|
|
|
186
|
+ let rawInput = meetLinkField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
187
|
+ guard rawInput.isEmpty == false else {
|
|
|
188
|
+ showSimpleAlert(title: "Join Meeting", message: "Please enter a Google Meet link.")
|
|
|
189
|
+ return
|
|
|
190
|
+ }
|
|
|
191
|
+
|
|
|
192
|
+ let normalized = normalizedURLString(from: rawInput)
|
|
|
193
|
+ guard let url = URL(string: normalized),
|
|
|
194
|
+ let host = url.host?.lowercased(),
|
|
|
195
|
+ host.contains("meet.google.com") else {
|
|
|
196
|
+ showSimpleAlert(title: "Invalid Link", message: "Please enter a valid Google Meet link.")
|
|
|
197
|
+ return
|
|
|
198
|
+ }
|
|
|
199
|
+
|
|
|
200
|
+ openInSafari(url: url)
|
|
|
201
|
+ }
|
|
|
202
|
+
|
|
|
203
|
+ @objc private func cancelMeetJoinClicked(_ sender: Any?) {
|
|
|
204
|
+ meetLinkField?.stringValue = ""
|
|
|
205
|
+ }
|
|
|
206
|
+
|
|
|
207
|
+ private func normalizedURLString(from value: String) -> String {
|
|
|
208
|
+ if value.lowercased().hasPrefix("http://") || value.lowercased().hasPrefix("https://") {
|
|
|
209
|
+ return value
|
|
|
210
|
+ }
|
|
|
211
|
+ return "https://\(value)"
|
|
|
212
|
+ }
|
|
|
213
|
+
|
|
|
214
|
+ private func openInAppBrowser(with url: URL) {
|
|
|
215
|
+ let browserController: InAppBrowserWindowController
|
|
|
216
|
+ if let existing = inAppBrowserWindowController {
|
|
|
217
|
+ browserController = existing
|
|
|
218
|
+ } else {
|
|
|
219
|
+ browserController = InAppBrowserWindowController()
|
|
|
220
|
+ inAppBrowserWindowController = browserController
|
|
|
221
|
+ }
|
|
|
222
|
+
|
|
|
223
|
+ browserController.load(url: url)
|
|
|
224
|
+ browserController.showWindow(nil)
|
|
|
225
|
+ browserController.window?.makeKeyAndOrderFront(nil)
|
|
|
226
|
+ browserController.window?.orderFrontRegardless()
|
|
|
227
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
228
|
+ }
|
|
|
229
|
+
|
|
|
230
|
+ private func openInSafari(url: URL) {
|
|
|
231
|
+ guard let safariAppURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Safari") else {
|
|
|
232
|
+ NSWorkspace.shared.open(url)
|
|
|
233
|
+ return
|
|
|
234
|
+ }
|
|
|
235
|
+
|
|
|
236
|
+ let configuration = NSWorkspace.OpenConfiguration()
|
|
|
237
|
+ NSWorkspace.shared.open([url], withApplicationAt: safariAppURL, configuration: configuration) { _, error in
|
|
|
238
|
+ if let error {
|
|
|
239
|
+ self.showSimpleAlert(title: "Unable to Open Safari", message: error.localizedDescription)
|
|
|
240
|
+ }
|
|
|
241
|
+ }
|
|
|
242
|
+ }
|
|
|
243
|
+
|
|
182
|
244
|
private func showSidebarPage(_ page: SidebarPage) {
|
|
183
|
245
|
selectedSidebarPage = page
|
|
184
|
246
|
updateSidebarAppearance()
|
|
|
@@ -663,6 +725,7 @@ private extension ViewController {
|
|
663
|
725
|
codeField.textColor = palette.textPrimary
|
|
664
|
726
|
codeField.placeholderString = "Enter Link"
|
|
665
|
727
|
codeInputShell.addSubview(codeField)
|
|
|
728
|
+ meetLinkField = codeField
|
|
666
|
729
|
codeCard.addSubview(codeTitle)
|
|
667
|
730
|
codeCard.addSubview(codeInputShell)
|
|
668
|
731
|
|
|
|
@@ -718,11 +781,40 @@ private extension ViewController {
|
|
718
|
781
|
let spacer = NSView()
|
|
719
|
782
|
spacer.translatesAutoresizingMaskIntoConstraints = false
|
|
720
|
783
|
row.addArrangedSubview(spacer)
|
|
721
|
|
- row.addArrangedSubview(actionButton(title: "Cancel", color: palette.cancelButton, textColor: palette.textSecondary, width: 110))
|
|
722
|
|
- row.addArrangedSubview(actionButton(title: "Join", color: palette.primaryBlue, textColor: .white, width: 116))
|
|
|
784
|
+ row.addArrangedSubview(meetActionButton(
|
|
|
785
|
+ title: "Cancel",
|
|
|
786
|
+ color: palette.cancelButton,
|
|
|
787
|
+ textColor: palette.textSecondary,
|
|
|
788
|
+ width: 110,
|
|
|
789
|
+ action: #selector(cancelMeetJoinClicked(_:))
|
|
|
790
|
+ ))
|
|
|
791
|
+ row.addArrangedSubview(meetActionButton(
|
|
|
792
|
+ title: "Join",
|
|
|
793
|
+ color: palette.primaryBlue,
|
|
|
794
|
+ textColor: .white,
|
|
|
795
|
+ width: 116,
|
|
|
796
|
+ action: #selector(joinMeetClicked(_:))
|
|
|
797
|
+ ))
|
|
723
|
798
|
return row
|
|
724
|
799
|
}
|
|
725
|
800
|
|
|
|
801
|
+ func meetActionButton(title: String, color: NSColor, textColor: NSColor, width: CGFloat, action: Selector) -> NSButton {
|
|
|
802
|
+ let button = NSButton(title: title, target: self, action: action)
|
|
|
803
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
804
|
+ button.isBordered = false
|
|
|
805
|
+ button.bezelStyle = .regularSquare
|
|
|
806
|
+ button.wantsLayer = true
|
|
|
807
|
+ button.layer?.cornerRadius = 9
|
|
|
808
|
+ button.layer?.backgroundColor = color.cgColor
|
|
|
809
|
+ button.layer?.borderColor = (title == "Cancel" ? palette.inputBorder : palette.primaryBlueBorder).cgColor
|
|
|
810
|
+ button.layer?.borderWidth = 1
|
|
|
811
|
+ button.font = typography.buttonText
|
|
|
812
|
+ button.contentTintColor = textColor
|
|
|
813
|
+ button.widthAnchor.constraint(equalToConstant: width).isActive = true
|
|
|
814
|
+ button.heightAnchor.constraint(equalToConstant: 36).isActive = true
|
|
|
815
|
+ return button
|
|
|
816
|
+ }
|
|
|
817
|
+
|
|
726
|
818
|
func makePaywallContent() -> NSView {
|
|
727
|
819
|
paywallPlanViews.removeAll()
|
|
728
|
820
|
premiumPlanByView.removeAll()
|
|
|
@@ -1451,9 +1543,7 @@ private extension ViewController {
|
|
1451
|
1543
|
/// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
|
|
1452
|
1544
|
private class RowHitTestView: NSView {
|
|
1453
|
1545
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
1454
|
|
- guard let superview else { return nil }
|
|
1455
|
|
- let local = convert(point, from: superview)
|
|
1456
|
|
- return bounds.contains(local) ? self : nil
|
|
|
1546
|
+ return bounds.contains(point) ? self : nil
|
|
1457
|
1547
|
}
|
|
1458
|
1548
|
}
|
|
1459
|
1549
|
|
|
|
@@ -2000,3 +2090,66 @@ private struct Typography {
|
|
2000
|
2090
|
let cardTime = NSFont.systemFont(ofSize: 12, weight: .regular)
|
|
2001
|
2091
|
}
|
|
2002
|
2092
|
|
|
|
2093
|
+private final class InAppBrowserWindowController: NSWindowController {
|
|
|
2094
|
+ private let browserViewController = InAppBrowserViewController()
|
|
|
2095
|
+
|
|
|
2096
|
+ init() {
|
|
|
2097
|
+ let browserWindow = NSWindow(
|
|
|
2098
|
+ contentRect: NSRect(x: 0, y: 0, width: 1100, height: 760),
|
|
|
2099
|
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
|
|
2100
|
+ backing: .buffered,
|
|
|
2101
|
+ defer: false
|
|
|
2102
|
+ )
|
|
|
2103
|
+ browserWindow.title = "Google Meet"
|
|
|
2104
|
+ browserWindow.center()
|
|
|
2105
|
+ browserWindow.contentViewController = browserViewController
|
|
|
2106
|
+ super.init(window: browserWindow)
|
|
|
2107
|
+ }
|
|
|
2108
|
+
|
|
|
2109
|
+ @available(*, unavailable)
|
|
|
2110
|
+ required init?(coder: NSCoder) {
|
|
|
2111
|
+ nil
|
|
|
2112
|
+ }
|
|
|
2113
|
+
|
|
|
2114
|
+ func load(url: URL) {
|
|
|
2115
|
+ browserViewController.load(url: url)
|
|
|
2116
|
+ }
|
|
|
2117
|
+}
|
|
|
2118
|
+
|
|
|
2119
|
+private final class InAppBrowserViewController: NSViewController, WKNavigationDelegate {
|
|
|
2120
|
+ private let webView = WKWebView(frame: .zero)
|
|
|
2121
|
+ private var lastLoadedURL: URL?
|
|
|
2122
|
+
|
|
|
2123
|
+ override func loadView() {
|
|
|
2124
|
+ webView.navigationDelegate = self
|
|
|
2125
|
+ webView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2126
|
+ view = webView
|
|
|
2127
|
+ }
|
|
|
2128
|
+
|
|
|
2129
|
+ func load(url: URL) {
|
|
|
2130
|
+ lastLoadedURL = url
|
|
|
2131
|
+ webView.load(URLRequest(url: url))
|
|
|
2132
|
+ }
|
|
|
2133
|
+
|
|
|
2134
|
+ func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
|
|
2135
|
+ let nsError = error as NSError
|
|
|
2136
|
+ if nsError.code == NSURLErrorCancelled {
|
|
|
2137
|
+ return
|
|
|
2138
|
+ }
|
|
|
2139
|
+ let alert = NSAlert()
|
|
|
2140
|
+ alert.messageText = "Unable to Open Link"
|
|
|
2141
|
+ alert.informativeText = "Could not load this page in the in-app browser.\n\n\(error.localizedDescription)\n\nOpen in default browser instead?"
|
|
|
2142
|
+ alert.addButton(withTitle: "Open in Browser")
|
|
|
2143
|
+ alert.addButton(withTitle: "Cancel")
|
|
|
2144
|
+ let result = alert.runModal()
|
|
|
2145
|
+ if result == .alertFirstButtonReturn, let url = lastLoadedURL {
|
|
|
2146
|
+ NSWorkspace.shared.open(url)
|
|
|
2147
|
+ }
|
|
|
2148
|
+ }
|
|
|
2149
|
+
|
|
|
2150
|
+ func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
|
|
2151
|
+ // WebKit process can be terminated by the OS; retry current page once.
|
|
|
2152
|
+ webView.reload()
|
|
|
2153
|
+ }
|
|
|
2154
|
+}
|
|
|
2155
|
+
|