feat: add App Intents and device control widget
This commit is contained in:
43 files changed
+852
-1070
No files matched your search
@@ -0,0 +1,143 @@
|
||||
// jumpstart-wake-control
|
||||
// Control Center wake button. Device choices are the home-screen widget's dynamic
|
||||
// configuration, which JavaScript updates when the device list loads.
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct WakeDeviceControl: ControlWidget {
|
||||
static let kind: String = "wakecontrol"
|
||||
|
||||
var body: some ControlWidgetConfiguration {
|
||||
AppIntentControlConfiguration(
|
||||
kind: Self.kind,
|
||||
provider: WakeDeviceControlProvider()
|
||||
) { value in
|
||||
ControlWidgetButton(action: WakeControlIntent(deviceId: value.deviceId, deviceName: value.deviceName)) {
|
||||
Label(value.deviceName, systemImage: "bolt.fill")
|
||||
}
|
||||
}
|
||||
.displayName("Wake Device")
|
||||
.description("Quickly wake a device from Control Center.")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
extension WakeDeviceControl {
|
||||
struct Value {
|
||||
var deviceId: String
|
||||
var deviceName: String
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct WakeDeviceControlProvider: AppIntentControlValueProvider {
|
||||
func previewValue(configuration: WakeControlConfiguration) -> WakeDeviceControl.Value {
|
||||
WakeDeviceControl.Value(
|
||||
deviceId: configuration.device ?? "",
|
||||
deviceName: deviceName(for: configuration.device)
|
||||
)
|
||||
}
|
||||
|
||||
func currentValue(configuration: WakeControlConfiguration) async throws -> WakeDeviceControl.Value {
|
||||
WakeDeviceControl.Value(
|
||||
deviceId: configuration.device ?? "",
|
||||
deviceName: deviceName(for: configuration.device)
|
||||
)
|
||||
}
|
||||
|
||||
private func deviceName(for deviceId: String?) -> String {
|
||||
guard let deviceId, !deviceId.isEmpty, deviceId != "none" else {
|
||||
return "Select Device"
|
||||
}
|
||||
|
||||
let options = widgetConfigurationOptions(
|
||||
name: "DeviceControl",
|
||||
parameter: "device",
|
||||
fallback: []
|
||||
)
|
||||
return options.first { $0.value == deviceId }?.name ?? "Select Device"
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct WakeControlConfiguration: ControlConfigurationIntent {
|
||||
static let title: LocalizedStringResource = "Device to Wake"
|
||||
|
||||
@Parameter(title: "Device", optionsProvider: WakeControlDeviceOptionsProvider())
|
||||
var device: String?
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct WakeControlDeviceOptionsProvider: DynamicOptionsProvider {
|
||||
func results() async throws -> IntentItemCollection<String> {
|
||||
let options = widgetConfigurationOptions(
|
||||
name: "DeviceControl",
|
||||
parameter: "device",
|
||||
fallback: [
|
||||
WidgetConfigurationOption(name: "Select a Device", value: "none")
|
||||
]
|
||||
)
|
||||
|
||||
return IntentItemCollection(sections: [
|
||||
IntentItemSection(items: options.map { option in
|
||||
IntentItem(
|
||||
option.value,
|
||||
title: LocalizedStringResource(stringLiteral: option.name),
|
||||
subtitle: option.subtitle.map { LocalizedStringResource(stringLiteral: $0) }
|
||||
)
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
func defaultResult() async -> String? {
|
||||
let options = widgetConfigurationOptions(
|
||||
name: "DeviceControl",
|
||||
parameter: "device",
|
||||
fallback: []
|
||||
)
|
||||
return options.first { $0.value != "none" }?.value ?? options.first?.value
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct WakeControlIntent: AppIntent {
|
||||
static let title: LocalizedStringResource = "Wake Device"
|
||||
static let description = IntentDescription("Wakes the selected device using Wake-on-LAN.")
|
||||
static let openAppWhenRun: Bool = true
|
||||
static let isDiscoverable: Bool = false
|
||||
|
||||
@Parameter(title: "Device ID")
|
||||
var deviceId: String
|
||||
|
||||
@Parameter(title: "Device Name")
|
||||
var deviceName: String
|
||||
|
||||
init() {
|
||||
self.deviceId = ""
|
||||
self.deviceName = ""
|
||||
}
|
||||
|
||||
init(deviceId: String, deviceName: String) {
|
||||
self.deviceId = deviceId
|
||||
self.deviceName = deviceName
|
||||
}
|
||||
|
||||
func perform() async throws -> some IntentResult & OpensIntent {
|
||||
let encoded = deviceId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? deviceId
|
||||
if !encoded.isEmpty, encoded != "none", let url = URL(string: "remotewol-upsnap://action/wake/\(encoded)") {
|
||||
return .result(opensIntent: OpenURLIntent(url))
|
||||
}
|
||||
|
||||
guard let fallback = URL(string: "remotewol-upsnap://") else {
|
||||
throw WakeControlIntentError.invalidDevice
|
||||
}
|
||||
return .result(opensIntent: OpenURLIntent(fallback))
|
||||
}
|
||||
}
|
||||
|
||||
enum WakeControlIntentError: LocalizedError {
|
||||
case invalidDevice
|
||||
|
||||
var errorDescription: String? {
|
||||
"Select a device to wake."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { withDangerousMod } = require('expo/config-plugins');
|
||||
|
||||
const MARKER = 'jumpstart-wake-control';
|
||||
|
||||
/**
|
||||
* Adds the Control Center wake control to the extension expo-widgets generates.
|
||||
* This plugin must be listed before expo-widgets so it runs after that extension is written.
|
||||
*/
|
||||
function withWakeControl(config) {
|
||||
return withDangerousMod(config, [
|
||||
'ios',
|
||||
async config => {
|
||||
const indexPath = path.join(
|
||||
config.modRequest.platformProjectRoot,
|
||||
'ExpoWidgetsTarget',
|
||||
'index.swift'
|
||||
);
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
throw new Error(
|
||||
'expo-widgets has not generated ios/ExpoWidgetsTarget/index.swift yet. List ./plugins/withWakeControl before expo-widgets in the plugins array.'
|
||||
);
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(indexPath, 'utf8');
|
||||
if (source.includes(MARKER)) {
|
||||
return config;
|
||||
}
|
||||
|
||||
if (!source.includes('WidgetLiveActivity()')) {
|
||||
throw new Error(
|
||||
'Could not find WidgetLiveActivity() in ios/ExpoWidgetsTarget/index.swift.'
|
||||
);
|
||||
}
|
||||
|
||||
const controlPath = path.join(
|
||||
config.modRequest.projectRoot,
|
||||
'plugins',
|
||||
'WakeDeviceControl.swift'
|
||||
);
|
||||
const controlSource = fs.readFileSync(controlPath, 'utf8');
|
||||
let next = source;
|
||||
|
||||
if (!next.includes('import AppIntents')) {
|
||||
next = next.replace('import SwiftUI\n', 'import SwiftUI\nimport AppIntents\n');
|
||||
}
|
||||
|
||||
next = next.replace(
|
||||
/^([ \t]*)WidgetLiveActivity\(\)/m,
|
||||
`$1if #available(iOS 18.0, *) {\n$1 WakeDeviceControl()\n$1}\n$1WidgetLiveActivity()`
|
||||
);
|
||||
next = `${next.trimEnd()}\n\n${controlSource.trim()}\n`;
|
||||
fs.writeFileSync(indexPath, next);
|
||||
return config;
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
module.exports = withWakeControl;
|
||||
Reference in new issue
Block a user