mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 05:02:50 +02:00
feat(ui): add family controls, casting and native notifications
This commit is contained in:
1 parent
522c46ed6f
commit
0622afcae9
11 files changed
+316
-7
No files matched your search
@@ -17,6 +17,7 @@
|
||||
78427FC66780623F31E7501E /* FrameControlApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C8E0D7C3F4F628941B3D5A /* FrameControlApp.swift */; };
|
||||
84423CB45629465420180A64 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8F2CB550FC81C01E6BDD5A71 /* Assets.xcassets */; };
|
||||
9657F7BC23E3352E5AB30777 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB544223FC60A59CC3E8EF5F /* SetupView.swift */; };
|
||||
A0C5B00E257230A38DBD9E54 /* ComfortNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81218B75FEEE47B8D8BAE20 /* ComfortNotificationTests.swift */; };
|
||||
A8C7AED25A6280682FCE45DC /* Citadel in Frameworks */ = {isa = PBXBuildFile; productRef = 6BA549B6CC0A0CB847126456 /* Citadel */; };
|
||||
DC043FB74BE2D23F3A5826BF /* FrameControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1740B691F9C25E5FB6F9EFC3 /* FrameControlTests.swift */; };
|
||||
E6898C714A92D3979F73B6E1 /* FrameFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F288DF6636A417F0CA3A6CD /* FrameFinder.swift */; };
|
||||
@@ -48,6 +49,7 @@
|
||||
BF0FCA7117DA3ABA449B4EE0 /* InstallLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallLink.swift; sourceTree = "<group>"; };
|
||||
D6C4E6C28315CA8729FCAAEA /* WebShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebShell.swift; sourceTree = "<group>"; };
|
||||
DB544223FC60A59CC3E8EF5F /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
|
||||
E81218B75FEEE47B8D8BAE20 /* ComfortNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComfortNotificationTests.swift; sourceTree = "<group>"; };
|
||||
EDC7BA8014DBC302D08FD397 /* HeadsetServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadsetServer.swift; sourceTree = "<group>"; };
|
||||
F3E2F5607DD877272483D64E /* FrameControl.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = FrameControl.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -107,6 +109,7 @@
|
||||
75A17B1C79C8C3C60FEABBA6 /* FrameControlTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E81218B75FEEE47B8D8BAE20 /* ComfortNotificationTests.swift */,
|
||||
1740B691F9C25E5FB6F9EFC3 /* FrameControlTests.swift */,
|
||||
);
|
||||
path = FrameControlTests;
|
||||
@@ -274,6 +277,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A0C5B00E257230A38DBD9E54 /* ComfortNotificationTests.swift in Sources */,
|
||||
DC043FB74BE2D23F3A5826BF /* FrameControlTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import WebKit
|
||||
import UserNotifications
|
||||
|
||||
/// The Frame Control page, served by the server on the headset, in a web view.
|
||||
/// window.frameApp (the same bridge the desktop app's preload.js provides) lets
|
||||
@@ -48,6 +49,7 @@ struct WebShell: UIViewRepresentable {
|
||||
let installCb = null;
|
||||
window.frameApp = {
|
||||
platform: "ios",
|
||||
notify: (message, request) => call("notify", { message, request }),
|
||||
readClipboard: () => call("readClipboard"),
|
||||
setUpConnection: () => call("setUpConnection"),
|
||||
open: (what) => call("open", what),
|
||||
@@ -58,13 +60,31 @@ struct WebShell: UIViewRepresentable {
|
||||
})();
|
||||
"""
|
||||
|
||||
final class Coordinator: NSObject, WKScriptMessageHandlerWithReply, WKNavigationDelegate, WKUIDelegate {
|
||||
final class Coordinator: NSObject, WKScriptMessageHandlerWithReply, WKNavigationDelegate, WKUIDelegate, UNUserNotificationCenterDelegate {
|
||||
let model: AppModel
|
||||
weak var web: WKWebView?
|
||||
var loaded: URL?
|
||||
private var installReady = false
|
||||
|
||||
init(model: AppModel) { self.model = model }
|
||||
init(model: AppModel) {
|
||||
self.model = model
|
||||
super.init()
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
completionHandler([.banner, .sound, .list])
|
||||
}
|
||||
|
||||
static func notificationContent(_ message: String) -> UNMutableNotificationContent? {
|
||||
guard !message.isEmpty, message.count <= 500 else { return nil }
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Frame Control"
|
||||
content.body = message
|
||||
content.sound = .default
|
||||
return content
|
||||
}
|
||||
|
||||
// MARK: bridge
|
||||
|
||||
@@ -76,6 +96,28 @@ struct WebShell: UIViewRepresentable {
|
||||
}
|
||||
let arg = body["arg"]
|
||||
switch name {
|
||||
case "notify":
|
||||
guard message.frameInfo.isMainFrame,
|
||||
message.frameInfo.securityOrigin.host == "127.0.0.1",
|
||||
let args = arg as? [String: Any], let text = args["message"] as? String,
|
||||
let content = Self.notificationContent(text) else {
|
||||
return replyHandler(nil, "Invalid notification")
|
||||
}
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let send: (Bool, Error?) -> Void = { allowed, error in
|
||||
guard allowed else {
|
||||
return replyHandler(nil, error?.localizedDescription ?? "Notifications are off. Enable them in iOS Settings.")
|
||||
}
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
center.add(request) { error in replyHandler(error == nil, error?.localizedDescription) }
|
||||
}
|
||||
if args["request"] as? Bool == true {
|
||||
center.requestAuthorization(options: [.alert, .sound], completionHandler: send)
|
||||
} else {
|
||||
center.getNotificationSettings { settings in
|
||||
send(settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional, nil)
|
||||
}
|
||||
}
|
||||
case "readClipboard":
|
||||
replyHandler(UIPasteboard.general.string ?? "", nil)
|
||||
case "setUpConnection":
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import XCTest
|
||||
import UserNotifications
|
||||
@testable import Frame_Control
|
||||
|
||||
final class ComfortNotificationTests: XCTestCase {
|
||||
func testNotificationContentAndBounds() {
|
||||
let content = WebShell.Coordinator.notificationContent("Time for a break")
|
||||
XCTAssertEqual(content?.title, "Frame Control")
|
||||
XCTAssertEqual(content?.body, "Time for a break")
|
||||
XCTAssertNotNil(content?.sound)
|
||||
XCTAssertNil(WebShell.Coordinator.notificationContent(""))
|
||||
XCTAssertNil(WebShell.Coordinator.notificationContent(String(repeating: "x", count: 501)))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user