Merge pull request 'feat: add App Intents and device control widget' (#3) from t3/expo-widgets-app-intents into t3/upgrade-expo-sdk-58
No files matched your search
@@ -41,3 +41,7 @@ app-example
|
|||||||
# generated native folders
|
# generated native folders
|
||||||
/ios
|
/ios
|
||||||
/android
|
/android
|
||||||
|
# @generated expo skills start
|
||||||
|
.agents/skills/expo-app-intents
|
||||||
|
.claude/skills/expo-app-intents
|
||||||
|
# @generated expo skills end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
internal import ExpoAppIntents
|
||||||
|
internal import ExpoModulesCore
|
||||||
|
|
||||||
|
/// Registered Expo inline module that wires app-target App Intents code to expo-app-intents.
|
||||||
|
/// Do not change the name of this class.
|
||||||
|
final class AppIntentsSetup: Module {
|
||||||
|
public func definition() -> ExpoModulesCore.ModuleDefinition {
|
||||||
|
Name("AppIntentsSetup")
|
||||||
|
|
||||||
|
OnCreate {
|
||||||
|
if #available(iOS 18.0, *) {
|
||||||
|
Task {
|
||||||
|
await AppIntentDispatcher.shared.setShortcutsRefreshHandler {
|
||||||
|
AppShortcuts.updateAppShortcutParameters()
|
||||||
|
}
|
||||||
|
AppShortcuts.updateAppShortcutParameters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import AppIntents
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
struct AppShortcuts: AppShortcutsProvider {
|
||||||
|
static var appShortcuts: [AppShortcut] {
|
||||||
|
AppShortcut(
|
||||||
|
intent: WakeDeviceIntent(),
|
||||||
|
phrases: [
|
||||||
|
"Wake a device in \(.applicationName)",
|
||||||
|
"Wake \(\.$device) in \(.applicationName)",
|
||||||
|
],
|
||||||
|
shortTitle: "Wake Device",
|
||||||
|
systemImageName: "bolt.fill"
|
||||||
|
)
|
||||||
|
AppShortcut(
|
||||||
|
intent: SleepDeviceIntent(),
|
||||||
|
phrases: [
|
||||||
|
"Sleep a device in \(.applicationName)",
|
||||||
|
"Sleep \(\.$device) in \(.applicationName)",
|
||||||
|
],
|
||||||
|
shortTitle: "Sleep Device",
|
||||||
|
systemImageName: "moon.fill"
|
||||||
|
)
|
||||||
|
AppShortcut(
|
||||||
|
intent: RestartDeviceIntent(),
|
||||||
|
phrases: [
|
||||||
|
"Restart a device in \(.applicationName)",
|
||||||
|
"Restart \(\.$device) in \(.applicationName)",
|
||||||
|
],
|
||||||
|
shortTitle: "Restart Device",
|
||||||
|
systemImageName: "arrow.clockwise"
|
||||||
|
)
|
||||||
|
AppShortcut(
|
||||||
|
intent: ShutdownDeviceIntent(),
|
||||||
|
phrases: [
|
||||||
|
"Shut down a device in \(.applicationName)",
|
||||||
|
"Shut down \(\.$device) in \(.applicationName)",
|
||||||
|
],
|
||||||
|
shortTitle: "Shut Down Device",
|
||||||
|
systemImageName: "power"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import AppIntents
|
||||||
|
|
||||||
|
enum DeviceActionPath: String {
|
||||||
|
case wake
|
||||||
|
case sleep
|
||||||
|
case restart
|
||||||
|
case shutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DeviceActionIntentError: LocalizedError {
|
||||||
|
case invalidDevice
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
"That device can't be opened."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deviceActionURL(_ action: DeviceActionPath, deviceId: String) throws -> URL {
|
||||||
|
let encoded = deviceId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? deviceId
|
||||||
|
guard !encoded.isEmpty,
|
||||||
|
let url = URL(string: "remotewol-upsnap://action/\(action.rawValue)/\(encoded)") else {
|
||||||
|
throw DeviceActionIntentError.invalidDevice
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
struct WakeDeviceIntent: AppIntent {
|
||||||
|
static let title: LocalizedStringResource = "Wake Device"
|
||||||
|
static let description = IntentDescription("Wakes the selected device using Wake-on-LAN.")
|
||||||
|
static let openAppWhenRun = true
|
||||||
|
|
||||||
|
@Parameter(title: "Device", requestValueDialog: "Which device should be woken?")
|
||||||
|
var device: DeviceEntity
|
||||||
|
|
||||||
|
static var parameterSummary: some ParameterSummary {
|
||||||
|
Summary("Wake \(\.$device)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func perform() async throws -> some IntentResult & OpensIntent {
|
||||||
|
let url = try deviceActionURL(.wake, deviceId: device.id)
|
||||||
|
return .result(opensIntent: OpenURLIntent(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
struct SleepDeviceIntent: AppIntent {
|
||||||
|
static let title: LocalizedStringResource = "Sleep Device"
|
||||||
|
static let description = IntentDescription("Sends the selected device to sleep.")
|
||||||
|
static let openAppWhenRun = true
|
||||||
|
|
||||||
|
@Parameter(title: "Device", requestValueDialog: "Which device should sleep?")
|
||||||
|
var device: DeviceEntity
|
||||||
|
|
||||||
|
static var parameterSummary: some ParameterSummary {
|
||||||
|
Summary("Sleep \(\.$device)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func perform() async throws -> some IntentResult & OpensIntent {
|
||||||
|
let url = try deviceActionURL(.sleep, deviceId: device.id)
|
||||||
|
return .result(opensIntent: OpenURLIntent(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
struct RestartDeviceIntent: AppIntent {
|
||||||
|
static let title: LocalizedStringResource = "Restart Device"
|
||||||
|
static let description = IntentDescription("Restarts the selected device.")
|
||||||
|
static let openAppWhenRun = true
|
||||||
|
|
||||||
|
@Parameter(title: "Device", requestValueDialog: "Which device should restart?")
|
||||||
|
var device: DeviceEntity
|
||||||
|
|
||||||
|
static var parameterSummary: some ParameterSummary {
|
||||||
|
Summary("Restart \(\.$device)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func perform() async throws -> some IntentResult & OpensIntent {
|
||||||
|
let url = try deviceActionURL(.restart, deviceId: device.id)
|
||||||
|
return .result(opensIntent: OpenURLIntent(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
struct ShutdownDeviceIntent: AppIntent {
|
||||||
|
static let title: LocalizedStringResource = "Shut Down Device"
|
||||||
|
static let description = IntentDescription("Shuts down the selected device.")
|
||||||
|
static let openAppWhenRun = true
|
||||||
|
|
||||||
|
@Parameter(title: "Device", requestValueDialog: "Which device should shut down?")
|
||||||
|
var device: DeviceEntity
|
||||||
|
|
||||||
|
static var parameterSummary: some ParameterSummary {
|
||||||
|
Summary("Shut down \(\.$device)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func perform() async throws -> some IntentResult & OpensIntent {
|
||||||
|
let url = try deviceActionURL(.shutdown, deviceId: device.id)
|
||||||
|
return .result(opensIntent: OpenURLIntent(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import AppIntents
|
||||||
|
internal import ExpoAppIntents
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
private func normalizeDeviceText(_ value: String) -> String {
|
||||||
|
value
|
||||||
|
.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current)
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DeviceEntity: AppEntity {
|
||||||
|
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Device"
|
||||||
|
static let defaultQuery = DeviceQuery()
|
||||||
|
|
||||||
|
var id: String
|
||||||
|
@Property(title: "Name")
|
||||||
|
var name: String
|
||||||
|
var subtitle: String?
|
||||||
|
var synonyms: [String]
|
||||||
|
|
||||||
|
var displayRepresentation: DisplayRepresentation {
|
||||||
|
if let subtitle {
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
|
return DisplayRepresentation(
|
||||||
|
title: "\(name)",
|
||||||
|
subtitle: "\(subtitle)",
|
||||||
|
synonyms: synonyms.map { LocalizedStringResource(stringLiteral: $0) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return DisplayRepresentation(title: "\(name)", subtitle: "\(subtitle)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
|
return DisplayRepresentation(
|
||||||
|
title: "\(name)",
|
||||||
|
synonyms: synonyms.map { LocalizedStringResource(stringLiteral: $0) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return DisplayRepresentation(title: "\(name)")
|
||||||
|
}
|
||||||
|
|
||||||
|
init(record: AppIntentEntityRecord) {
|
||||||
|
self.id = record.id
|
||||||
|
self.subtitle = record.subtitle
|
||||||
|
self.synonyms = record.synonyms
|
||||||
|
self.name = record.title
|
||||||
|
}
|
||||||
|
|
||||||
|
func matches(_ searchText: String) -> Bool {
|
||||||
|
let normalizedSearch = normalizeDeviceText(searchText)
|
||||||
|
guard !normalizedSearch.isEmpty else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ([name, id] + synonyms).map(normalizeDeviceText).contains { term in
|
||||||
|
normalizedSearch == term || normalizedSearch.contains(term) || term.contains(normalizedSearch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import AppIntents
|
||||||
|
internal import ExpoAppIntents
|
||||||
|
|
||||||
|
/// Resolves devices from the catalog published by `syncDevicesToWidget`.
|
||||||
|
struct DeviceQuery: EntityStringQuery {
|
||||||
|
func entities(for identifiers: [String]) async throws -> [DeviceEntity] {
|
||||||
|
try await AppIntentEntityStore.shared
|
||||||
|
.entities(ofKind: "device", matching: identifiers)
|
||||||
|
.map(DeviceEntity.init(record:))
|
||||||
|
}
|
||||||
|
|
||||||
|
func suggestedEntities() async throws -> [DeviceEntity] {
|
||||||
|
try await AppIntentEntityStore.shared
|
||||||
|
.entities(ofKind: "device")
|
||||||
|
.map(DeviceEntity.init(record:))
|
||||||
|
}
|
||||||
|
|
||||||
|
func entities(matching string: String) async throws -> [DeviceEntity] {
|
||||||
|
let search = string.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !search.isEmpty else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return try await suggestedEntities().filter { $0.matches(search) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
"appleTeamId": "8S7C654DQ4",
|
"appleTeamId": "8S7C654DQ4",
|
||||||
"entitlements": {
|
"entitlements": {
|
||||||
"com.apple.security.application-groups": [
|
"com.apple.security.application-groups": [
|
||||||
"group.abunchofknowitalls.remotewol-upsnap",
|
|
||||||
"group.abunchofknowitalls.remotewol-upsnap"
|
"group.abunchofknowitalls.remotewol-upsnap"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -50,35 +49,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@bacons/apple-targets",
|
"./plugins/withWakeControl.js",
|
||||||
|
[
|
||||||
|
"expo-widgets",
|
||||||
|
{
|
||||||
|
"bundleIdentifier": "com.abunchofknowitalls.remotewol-upsnap.widget",
|
||||||
|
"groupIdentifier": "group.abunchofknowitalls.remotewol-upsnap",
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"name": "DeviceControl",
|
||||||
|
"displayName": "Device Control",
|
||||||
|
"description": "Control your devices with wake, sleep, restart, and shutdown actions.",
|
||||||
|
"ios": {
|
||||||
|
"supportedFamilies": ["systemSmall", "systemMedium"],
|
||||||
|
"initialLayout": "./src/widgets/DeviceControl.tsx",
|
||||||
|
"configuration": {
|
||||||
|
"title": "Device Control",
|
||||||
|
"description": "Select a device to control from your home screen.",
|
||||||
|
"parameters": {
|
||||||
|
"device": {
|
||||||
|
"title": "Device",
|
||||||
|
"type": "enum",
|
||||||
|
"default": "none",
|
||||||
|
"dynamic": true,
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"name": "Select a Device",
|
||||||
|
"value": "none"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"expo-font",
|
"expo-font",
|
||||||
"expo-image",
|
"expo-image",
|
||||||
"expo-web-browser"
|
"expo-web-browser",
|
||||||
|
"expo-app-intents"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true,
|
"typedRoutes": true,
|
||||||
"reactCompiler": true
|
"reactCompiler": true,
|
||||||
|
"inlineModules": {
|
||||||
|
"watchedDirectories": [
|
||||||
|
"app-intents"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"router": {},
|
"router": {},
|
||||||
"eas": {
|
"eas": {
|
||||||
"build": {
|
|
||||||
"experimental": {
|
|
||||||
"ios": {
|
|
||||||
"appExtensions": [
|
|
||||||
{
|
|
||||||
"bundleIdentifier": "com.abunchofknowitalls.remotewol-upsnap.widget",
|
|
||||||
"targetName": "widget",
|
|
||||||
"entitlements": {
|
|
||||||
"com.apple.security.application-groups": [
|
|
||||||
"group.abunchofknowitalls.remotewol-upsnap"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"projectId": "b2c99399-8470-4a78-9253-492056fc1c35"
|
"projectId": "b2c99399-8470-4a78-9253-492056fc1c35"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
"name": "remote-wol",
|
"name": "remote-wol",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bacons/apple-targets": "^3.0.6",
|
|
||||||
"@expo/ui": "~58.0.21",
|
"@expo/ui": "~58.0.21",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
"burnt": "^0.13.0",
|
"burnt": "^0.13.0",
|
||||||
"expo": "^58.0.16",
|
"expo": "^58.0.16",
|
||||||
|
"expo-app-intents": "~0.5.10",
|
||||||
"expo-constants": "~58.0.12",
|
"expo-constants": "~58.0.12",
|
||||||
"expo-dev-client": "~58.0.14",
|
"expo-dev-client": "~58.0.14",
|
||||||
"expo-font": "~58.0.9",
|
"expo-font": "~58.0.9",
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"expo-symbols": "~58.0.4",
|
"expo-symbols": "~58.0.4",
|
||||||
"expo-system-ui": "~58.0.6",
|
"expo-system-ui": "~58.0.6",
|
||||||
"expo-web-browser": "~58.0.6",
|
"expo-web-browser": "~58.0.6",
|
||||||
|
"expo-widgets": "~58.0.21",
|
||||||
"react": "19.3.0",
|
"react": "19.3.0",
|
||||||
"react-dom": "19.3.0",
|
"react-dom": "19.3.0",
|
||||||
"react-native": "0.88.0",
|
"react-native": "0.88.0",
|
||||||
@@ -1137,29 +1138,6 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@bacons/apple-targets": {
|
|
||||||
"version": "3.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bacons/apple-targets/-/apple-targets-3.0.7.tgz",
|
|
||||||
"integrity": "sha512-RpzofKGvll8BPrvgf1Xj5raoHffJRW2qfWl7Q9F/25eo6lZeRa9R5NRIZCpUYoVnsL7whCQV9NIp0/ai4O+Ejw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@bacons/xcode": "1.0.0-alpha.27",
|
|
||||||
"@react-native/normalize-colors": "^0.79.2",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"glob": "^10.4.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bacons/xcode": {
|
|
||||||
"version": "1.0.0-alpha.27",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bacons/xcode/-/xcode-1.0.0-alpha.27.tgz",
|
|
||||||
"integrity": "sha512-WlemTiwpwwVH8IMvg4VBsyxGUuFWUfzeWKzxhU+dRxd8MHvnX7F9PlWCf7T0ZXmtjbl39iEgQB+/Tf3wuGJbyg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@expo/plist": "^0.0.18",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"uuid": "^8.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
@@ -2165,17 +2143,6 @@
|
|||||||
"integrity": "sha512-KtLrtAvpShq16l9lTxd2fZSjULxcGxXrJTHA31iUVqrc+Af7aTAJW4ZB3yQBgi/w+kSb6BB/CG9RXI0ohbMm6g==",
|
"integrity": "sha512-KtLrtAvpShq16l9lTxd2fZSjULxcGxXrJTHA31iUVqrc+Af7aTAJW4ZB3yQBgi/w+kSb6BB/CG9RXI0ohbMm6g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@expo/plist": {
|
|
||||||
"version": "0.0.18",
|
|
||||||
"resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.0.18.tgz",
|
|
||||||
"integrity": "sha512-+48gRqUiz65R21CZ/IXa7RNBXgAI/uPSdvJqoN9x1hfL44DNbUoWHgHiEXTx7XelcATpDwNTz6sHLfy0iNqf+w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@xmldom/xmldom": "~0.7.0",
|
|
||||||
"base64-js": "^1.2.3",
|
|
||||||
"xmlbuilder": "^14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@expo/prebuild-config": {
|
"node_modules/@expo/prebuild-config": {
|
||||||
"version": "58.0.14",
|
"version": "58.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-58.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-58.0.14.tgz",
|
||||||
@@ -2380,23 +2347,6 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
|
||||||
"version": "8.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
|
||||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"string-width": "^5.1.2",
|
|
||||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
|
||||||
"strip-ansi": "^7.0.1",
|
|
||||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
|
||||||
"wrap-ansi": "^8.1.0",
|
|
||||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@isaacs/ttlcache": {
|
"node_modules/@isaacs/ttlcache": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz",
|
||||||
@@ -2671,16 +2621,6 @@
|
|||||||
"url": "https://github.com/sponsors/oxc-project"
|
"url": "https://github.com/sponsors/oxc-project"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
|
||||||
"version": "0.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
|
||||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.2.0.tgz",
|
||||||
@@ -3251,12 +3191,6 @@
|
|||||||
"node": "^22.13.0 || ^24.3.0 || >= 26.0.0"
|
"node": "^22.13.0 || ^24.3.0 || >= 26.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-native/normalize-colors": {
|
|
||||||
"version": "0.79.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.7.tgz",
|
|
||||||
"integrity": "sha512-RrvewhdanEWhlyrHNWGXGZCc6MY0JGpNgRzA8y6OomDz0JmlnlIsbBHbNpPnIrt9Jh2KaV10KTscD1Ry8xU9gQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@react-native/virtualized-lists": {
|
"node_modules/@react-native/virtualized-lists": {
|
||||||
"version": "0.88.0",
|
"version": "0.88.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.88.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.88.0.tgz",
|
||||||
@@ -4035,16 +3969,6 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@xmldom/xmldom": {
|
|
||||||
"version": "0.7.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz",
|
|
||||||
"integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==",
|
|
||||||
"deprecated": "this version has critical issues, please update to the latest version",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/2g": {
|
"node_modules/2g": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/2g/-/2g-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/2g/-/2g-1.0.1.tgz",
|
||||||
@@ -4554,6 +4478,7 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
@@ -5388,12 +5313,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eastasianwidth": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -5406,12 +5325,6 @@
|
|||||||
"integrity": "sha512-9qpCBlXPy5v7n1Q7WTO5rW2StI4WNQhl96HpgXRZLPlNoXAD0LbsQUflovQYAO44Yia073ISusqohncvGSMbGA==",
|
"integrity": "sha512-9qpCBlXPy5v7n1Q7WTO5rW2StI4WNQhl96HpgXRZLPlNoXAD0LbsQUflovQYAO44Yia073ISusqohncvGSMbGA==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
|
||||||
"version": "9.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
|
||||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||||
@@ -6169,6 +6082,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-app-intents": {
|
||||||
|
"version": "0.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-app-intents/-/expo-app-intents-0.5.10.tgz",
|
||||||
|
"integrity": "sha512-PNxAbIaNjLDeajnVJGClDXnB1CFkGScuAYcEORFsmaLmABMjlxq/jNC708cgWVyDgX2XUvDBXifq2rqvQAsPmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/ui": "^58.0.21",
|
||||||
|
"commander": "^12.1.0",
|
||||||
|
"prompts": "^2.4.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"expo-app-intents": "bin/expo-app-intents.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-app-intents/node_modules/commander": {
|
||||||
|
"version": "12.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-asset": {
|
"node_modules/expo-asset": {
|
||||||
"version": "58.0.17",
|
"version": "58.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-58.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-58.0.17.tgz",
|
||||||
@@ -6604,6 +6545,50 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-widgets": {
|
||||||
|
"version": "58.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-widgets/-/expo-widgets-58.0.21.tgz",
|
||||||
|
"integrity": "sha512-0DiTq5jxNgiX7KPcu8GewlvDMxLcrcT4Q2gVyBTg67hzrC8KS4Du6ZbOCYflvaVrdqN8Mhhwtnrl4UFydoYK7Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/plist": "^0.10.1",
|
||||||
|
"@expo/ui": "~58.0.21"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-widgets/node_modules/@expo/plist": {
|
||||||
|
"version": "0.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.10.1.tgz",
|
||||||
|
"integrity": "sha512-nuNOqj+r/hGJbZ1Ax3jUpZaOB8UAMHUrISALJEUKaCieTflYj7224EqFWmMpriCGk9wXS1gH+9GGVnSj0tWzuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@xmldom/xmldom": "^0.8.8",
|
||||||
|
"base64-js": "^1.5.1",
|
||||||
|
"xmlbuilder": "^15.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-widgets/node_modules/@xmldom/xmldom": {
|
||||||
|
"version": "0.8.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz",
|
||||||
|
"integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-widgets/node_modules/xmlbuilder": {
|
||||||
|
"version": "15.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
|
||||||
|
"integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo/node_modules/@expo/cli": {
|
"node_modules/expo/node_modules/@expo/cli": {
|
||||||
"version": "58.1.13",
|
"version": "58.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-58.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-58.1.13.tgz",
|
||||||
@@ -7215,22 +7200,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/foreground-child": {
|
|
||||||
"version": "3.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
|
||||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"cross-spawn": "^7.0.6",
|
|
||||||
"signal-exit": "^4.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
@@ -7390,27 +7359,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
|
||||||
"version": "10.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
|
||||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
|
||||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"foreground-child": "^3.1.0",
|
|
||||||
"jackspeak": "^3.1.2",
|
|
||||||
"minimatch": "^9.0.4",
|
|
||||||
"minipass": "^7.1.2",
|
|
||||||
"package-json-from-dist": "^1.0.0",
|
|
||||||
"path-scurry": "^1.11.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"glob": "dist/esm/bin.mjs"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -7424,30 +7372,6 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
|
||||||
"version": "2.1.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz",
|
|
||||||
"integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob/node_modules/minimatch": {
|
|
||||||
"version": "9.0.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
|
||||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.17"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "14.0.0",
|
"version": "14.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||||
@@ -8274,21 +8198,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jackspeak": {
|
|
||||||
"version": "3.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
|
||||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@isaacs/cliui": "^8.0.2"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@pkgjs/parseargs": "^0.11.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jest-get-type": {
|
"node_modules/jest-get-type": {
|
||||||
"version": "29.6.3",
|
"version": "29.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
|
||||||
@@ -9928,12 +9837,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/package-json-from-dist": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
|
||||||
"license": "BlueOak-1.0.0"
|
|
||||||
},
|
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -9993,28 +9896,6 @@
|
|||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/path-scurry": {
|
|
||||||
"version": "1.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
|
||||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"lru-cache": "^10.2.0",
|
|
||||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/path-scurry/node_modules/lru-cache": {
|
|
||||||
"version": "10.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
|
||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -11112,18 +10993,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/signal-exit": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/simple-plist": {
|
"node_modules/simple-plist": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz",
|
||||||
@@ -11259,56 +11128,6 @@
|
|||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/string-width": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"eastasianwidth": "^0.2.0",
|
|
||||||
"emoji-regex": "^9.2.2",
|
|
||||||
"strip-ansi": "^7.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/string-width-cjs": {
|
|
||||||
"name": "string-width",
|
|
||||||
"version": "4.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
|
||||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"emoji-regex": "^8.0.0",
|
|
||||||
"is-fullwidth-code-point": "^3.0.0",
|
|
||||||
"strip-ansi": "^6.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-regex": "^5.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/string.prototype.matchall": {
|
"node_modules/string.prototype.matchall": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz",
|
||||||
@@ -11408,46 +11227,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-ansi": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-regex": "^6.2.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/strip-ansi-cjs": {
|
|
||||||
"name": "strip-ansi",
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-regex": "^5.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/strip-ansi/node_modules/ansi-regex": {
|
|
||||||
"version": "6.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz",
|
|
||||||
"integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/strip-bom": {
|
"node_modules/strip-bom": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||||
@@ -11977,16 +11756,6 @@
|
|||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
|
||||||
"version": "8.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
|
||||||
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"uuid": "dist/bin/uuid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/validate-npm-package-name": {
|
"node_modules/validate-npm-package-name": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz",
|
||||||
@@ -12177,85 +11946,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wrap-ansi": {
|
|
||||||
"version": "8.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
|
||||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^6.1.0",
|
|
||||||
"string-width": "^5.0.1",
|
|
||||||
"strip-ansi": "^7.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/wrap-ansi-cjs": {
|
|
||||||
"name": "wrap-ansi",
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^4.0.0",
|
|
||||||
"string-width": "^4.1.0",
|
|
||||||
"strip-ansi": "^6.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
|
||||||
"version": "4.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
|
||||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"emoji-regex": "^8.0.0",
|
|
||||||
"is-fullwidth-code-point": "^3.0.0",
|
|
||||||
"strip-ansi": "^6.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-regex": "^5.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/wrap-ansi/node_modules/ansi-styles": {
|
|
||||||
"version": "6.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
|
||||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "7.5.13",
|
"version": "7.5.13",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz",
|
||||||
@@ -12322,15 +12012,6 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/xmlbuilder": {
|
|
||||||
"version": "14.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz",
|
|
||||||
"integrity": "sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|||||||
@@ -11,12 +11,12 @@
|
|||||||
"lint": "expo lint"
|
"lint": "expo lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bacons/apple-targets": "^3.0.6",
|
|
||||||
"@expo/ui": "~58.0.21",
|
"@expo/ui": "~58.0.21",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
"burnt": "^0.13.0",
|
"burnt": "^0.13.0",
|
||||||
"expo": "^58.0.16",
|
"expo": "^58.0.16",
|
||||||
|
"expo-app-intents": "~0.5.10",
|
||||||
"expo-constants": "~58.0.12",
|
"expo-constants": "~58.0.12",
|
||||||
"expo-dev-client": "~58.0.14",
|
"expo-dev-client": "~58.0.14",
|
||||||
"expo-font": "~58.0.9",
|
"expo-font": "~58.0.9",
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"expo-symbols": "~58.0.4",
|
"expo-symbols": "~58.0.4",
|
||||||
"expo-system-ui": "~58.0.6",
|
"expo-system-ui": "~58.0.6",
|
||||||
"expo-web-browser": "~58.0.6",
|
"expo-web-browser": "~58.0.6",
|
||||||
|
"expo-widgets": "~58.0.21",
|
||||||
"react": "19.3.0",
|
"react": "19.3.0",
|
||||||
"react-dom": "19.3.0",
|
"react-dom": "19.3.0",
|
||||||
"react-native": "0.88.0",
|
"react-native": "0.88.0",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
import { syncDevicesToWidget } from '../services/widgetSync';
|
||||||
import { AuthResponse, User } from '../types';
|
import { AuthResponse, User } from '../types';
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
@@ -103,6 +104,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
try {
|
try {
|
||||||
await clearSession();
|
await clearSession();
|
||||||
|
syncDevicesToWidget([]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to logout', error);
|
console.error('Failed to logout', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,115 @@
|
|||||||
import { ExtensionStorage } from '@bacons/apple-targets';
|
import { setEntityCatalogAsync, isAvailable as appIntentsAvailable } from 'expo-app-intents';
|
||||||
|
import type { AppIntentEntity } from 'expo-app-intents';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
import { Device } from '../types';
|
import { Device } from '../types';
|
||||||
|
import DeviceControlWidget, { type WidgetDevice } from '../widgets/DeviceControl';
|
||||||
|
|
||||||
// Create a storage object with the App Group
|
const DEVICE_KIND = 'device';
|
||||||
const storage =
|
|
||||||
Platform.OS === 'ios'
|
|
||||||
? new ExtensionStorage('group.abunchofknowitalls.remotewol-upsnap')
|
|
||||||
: null;
|
|
||||||
|
|
||||||
interface WidgetDevice {
|
let pendingCatalog: AppIntentEntity[] | null = null;
|
||||||
id: string;
|
let catalogWrite: Promise<void> | null = null;
|
||||||
name: string;
|
|
||||||
mac: string;
|
function toWidgetDevice(device: Device): WidgetDevice | null {
|
||||||
ip: string;
|
const id = device.id.trim();
|
||||||
canSleep: boolean;
|
if (!id) {
|
||||||
canShutdown: boolean;
|
return null;
|
||||||
status: string;
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: device.name.trim() || device.mac.trim() || 'Device',
|
||||||
|
mac: device.mac,
|
||||||
|
ip: device.ip,
|
||||||
|
canSleep: device.sol_enabled,
|
||||||
|
canShutdown: device.shutdown_cmd !== '',
|
||||||
|
status: device.status || 'unknown',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCatalogEntity(device: WidgetDevice): AppIntentEntity {
|
||||||
|
const synonyms = [device.mac, device.ip].filter(value => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 && trimmed !== device.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: device.id,
|
||||||
|
title: device.name,
|
||||||
|
subtitle: device.mac.trim() || undefined,
|
||||||
|
synonyms,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishDeviceCatalog(entities: AppIntentEntity[]): void {
|
||||||
|
if (!appIntentsAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingCatalog = entities;
|
||||||
|
if (catalogWrite) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogWrite = drainDeviceCatalog();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drainDeviceCatalog(): Promise<void> {
|
||||||
|
while (pendingCatalog) {
|
||||||
|
const next = pendingCatalog;
|
||||||
|
pendingCatalog = null;
|
||||||
|
try {
|
||||||
|
await setEntityCatalogAsync(DEVICE_KIND, next);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not publish the device catalog.', error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogWrite = null;
|
||||||
|
if (pendingCatalog) {
|
||||||
|
catalogWrite = drainDeviceCatalog();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syncs device data to the iOS widget via App Group shared storage.
|
* Syncs device data to the iOS widget and Siri device catalog.
|
||||||
* This allows the widget to display device information and create action deep links.
|
* Called after the device list loads.
|
||||||
*/
|
*/
|
||||||
export function syncDevicesToWidget(devices: Device[]): void {
|
export function syncDevicesToWidget(devices: Device[]): void {
|
||||||
if (!storage) {
|
if (Platform.OS !== 'ios') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Transform devices to the format expected by the widget
|
const widgetDevices = devices
|
||||||
const widgetDevices: WidgetDevice[] = devices.map(device => ({
|
.map(toWidgetDevice)
|
||||||
id: device.id,
|
.filter((device): device is WidgetDevice => device !== null);
|
||||||
name: device.name,
|
|
||||||
mac: device.mac,
|
|
||||||
ip: device.ip,
|
|
||||||
canSleep: device.sol_enabled,
|
|
||||||
canShutdown: device.shutdown_cmd !== "",
|
|
||||||
status: device.status || 'unknown',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const devicesJson = JSON.stringify(widgetDevices);
|
DeviceControlWidget.setConfigurationParameterEnum(
|
||||||
storage.set('devices', devicesJson);
|
'device',
|
||||||
|
widgetDevices.map(device => ({
|
||||||
// Refresh widgets to pick up the new data
|
name: device.name,
|
||||||
ExtensionStorage.reloadWidget();
|
value: device.id,
|
||||||
|
subtitle: device.mac.trim() || undefined,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
DeviceControlWidget.updateSnapshot({ devices: widgetDevices });
|
||||||
|
publishDeviceCatalog(widgetDevices.map(toCatalogEntity));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to sync devices to widget:', error);
|
console.error('Failed to sync devices to widget:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refreshes all widget timelines to pick up new data.
|
* Refreshes widget timelines so they pick up the latest snapshot.
|
||||||
*/
|
*/
|
||||||
export function refreshWidgets(): void {
|
export function refreshWidgets(): void {
|
||||||
if (!storage) {
|
if (Platform.OS !== 'ios') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ExtensionStorage.reloadWidget();
|
DeviceControlWidget.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to refresh widgets:', error);
|
console.error('Failed to refresh widgets:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { HStack, Image, Link, Spacer, Text, VStack } from '@expo/ui/swift-ui';
|
||||||
|
import {
|
||||||
|
background,
|
||||||
|
containerBackground,
|
||||||
|
font,
|
||||||
|
foregroundStyle,
|
||||||
|
frame,
|
||||||
|
lineLimit,
|
||||||
|
padding,
|
||||||
|
shapes,
|
||||||
|
} from '@expo/ui/swift-ui/modifiers';
|
||||||
|
import { createWidget, type WidgetEnvironment } from 'expo-widgets';
|
||||||
|
import type { SFSymbol } from 'sf-symbols-typescript';
|
||||||
|
|
||||||
|
export type WidgetDevice = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
mac: string;
|
||||||
|
ip: string;
|
||||||
|
canSleep: boolean;
|
||||||
|
canShutdown: boolean;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeviceControlProps = {
|
||||||
|
devices: WidgetDevice[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeviceControlConfiguration = {
|
||||||
|
device?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeviceAction = {
|
||||||
|
id: 'wake' | 'sleep' | 'restart' | 'shutdown';
|
||||||
|
symbol: SFSymbol;
|
||||||
|
color: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeviceControlWidgetView = (
|
||||||
|
props: DeviceControlProps,
|
||||||
|
environment: WidgetEnvironment<DeviceControlConfiguration>
|
||||||
|
) => {
|
||||||
|
'widget';
|
||||||
|
const rootModifiers = [
|
||||||
|
containerBackground({ type: 'hierarchical', style: 'quaternary' }, 'widget'),
|
||||||
|
padding({ top: 4, bottom: 4 }),
|
||||||
|
];
|
||||||
|
const devices = Array.isArray(props.devices) ? props.devices : [];
|
||||||
|
const selectedId = environment.configuration?.device;
|
||||||
|
const device = devices.find(candidate => candidate.id === selectedId);
|
||||||
|
|
||||||
|
if (!device || !selectedId || selectedId === 'none') {
|
||||||
|
return (
|
||||||
|
<VStack
|
||||||
|
spacing={8}
|
||||||
|
modifiers={[...rootModifiers, frame({ maxWidth: 10000, maxHeight: 10000 })]}
|
||||||
|
>
|
||||||
|
<Image systemName="desktopcomputer" size={32} color="#8E8E93" />
|
||||||
|
<Text
|
||||||
|
modifiers={[
|
||||||
|
font({ size: 14, weight: 'medium' }),
|
||||||
|
foregroundStyle({ type: 'hierarchical', style: 'secondary' }),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Select a Device
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
modifiers={[
|
||||||
|
font({ size: 11 }),
|
||||||
|
foregroundStyle({ type: 'hierarchical', style: 'tertiary' }),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Long press to configure
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOnline = device.status === 'online';
|
||||||
|
const isOffline = device.status === 'offline';
|
||||||
|
const actions: DeviceAction[] = [
|
||||||
|
{ id: 'wake', symbol: 'bolt.fill', color: '#4CAF50', enabled: isOffline },
|
||||||
|
{
|
||||||
|
id: 'sleep',
|
||||||
|
symbol: 'moon.fill',
|
||||||
|
color: '#FF9800',
|
||||||
|
enabled: isOnline && Boolean(device.canSleep),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'restart',
|
||||||
|
symbol: 'arrow.clockwise',
|
||||||
|
color: '#2196F3',
|
||||||
|
enabled: isOnline && Boolean(device.canShutdown),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shutdown',
|
||||||
|
symbol: 'power',
|
||||||
|
color: '#F44336',
|
||||||
|
enabled: isOnline && Boolean(device.canShutdown),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const buttons = actions.map(action => {
|
||||||
|
const tint = action.enabled ? action.color : '#8E8E93';
|
||||||
|
const button = (
|
||||||
|
<VStack
|
||||||
|
alignment="center"
|
||||||
|
modifiers={[
|
||||||
|
frame({ maxWidth: 10000, minHeight: 36 }),
|
||||||
|
background(`${tint}26`, shapes.roundedRectangle({ cornerRadius: 8 })),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Image systemName={action.symbol} size={16} color={tint} />
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!action.enabled) {
|
||||||
|
return <VStack key={action.id}>{button}</VStack>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={action.id}
|
||||||
|
destination={`remotewol-upsnap://action/${action.id}/${encodeURIComponent(device.id)}`}
|
||||||
|
>
|
||||||
|
{button}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const header = (
|
||||||
|
<VStack alignment="leading" spacing={2}>
|
||||||
|
<Text modifiers={[font({ size: 15, weight: 'semibold' }), lineLimit(1)]}>{device.name}</Text>
|
||||||
|
<Text
|
||||||
|
modifiers={[
|
||||||
|
font({ size: 11 }),
|
||||||
|
foregroundStyle({ type: 'hierarchical', style: 'secondary' }),
|
||||||
|
lineLimit(1),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{device.mac}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (environment.widgetFamily === 'systemSmall') {
|
||||||
|
return (
|
||||||
|
<VStack alignment="leading" spacing={8} modifiers={rootModifiers}>
|
||||||
|
{header}
|
||||||
|
<Spacer minLength={4} />
|
||||||
|
<VStack spacing={6}>
|
||||||
|
<HStack spacing={6}>
|
||||||
|
{buttons[0]}
|
||||||
|
{buttons[1]}
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={6}>
|
||||||
|
{buttons[2]}
|
||||||
|
{buttons[3]}
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack alignment="leading" spacing={8} modifiers={rootModifiers}>
|
||||||
|
{header}
|
||||||
|
<Spacer minLength={4} />
|
||||||
|
<HStack spacing={8}>{buttons}</HStack>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeviceControlWidget = createWidget<DeviceControlProps, DeviceControlConfiguration>(
|
||||||
|
'DeviceControl',
|
||||||
|
DeviceControlWidgetView,
|
||||||
|
{ devices: [] }
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DeviceControlWidget;
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import WidgetKit
|
|
||||||
import AppIntents
|
|
||||||
|
|
||||||
// MARK: - Shared Data Access
|
|
||||||
|
|
||||||
struct SharedDeviceData {
|
|
||||||
static let appGroupIdentifier = "group.abunchofknowitalls.remotewol-upsnap"
|
|
||||||
|
|
||||||
static var sharedDefaults: UserDefaults? {
|
|
||||||
UserDefaults(suiteName: appGroupIdentifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func getDevices() -> [DeviceInfo] {
|
|
||||||
guard let defaults = sharedDefaults,
|
|
||||||
let jsonString = defaults.string(forKey: "devices"),
|
|
||||||
let data = jsonString.data(using: .utf8),
|
|
||||||
let devices = try? JSONDecoder().decode([DeviceInfo].self, from: data) else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return devices
|
|
||||||
}
|
|
||||||
|
|
||||||
static func getDevice(id: String) -> DeviceInfo? {
|
|
||||||
return getDevices().first { $0.id == id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Device Model
|
|
||||||
|
|
||||||
struct DeviceInfo: Codable, Hashable, Identifiable {
|
|
||||||
let id: String
|
|
||||||
let name: String
|
|
||||||
let mac: String
|
|
||||||
let ip: String
|
|
||||||
let canSleep: Bool
|
|
||||||
let canShutdown: Bool
|
|
||||||
let status: String
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Device Entity for AppIntents
|
|
||||||
|
|
||||||
struct DeviceEntity: AppEntity {
|
|
||||||
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Device"
|
|
||||||
static var defaultQuery = DeviceQuery()
|
|
||||||
|
|
||||||
var id: String
|
|
||||||
var name: String
|
|
||||||
var mac: String
|
|
||||||
|
|
||||||
var displayRepresentation: DisplayRepresentation {
|
|
||||||
DisplayRepresentation(title: "\(name)", subtitle: "\(mac)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DeviceQuery: EntityQuery {
|
|
||||||
func entities(for identifiers: [String]) async throws -> [DeviceEntity] {
|
|
||||||
let devices = SharedDeviceData.getDevices()
|
|
||||||
return devices
|
|
||||||
.filter { identifiers.contains($0.id) }
|
|
||||||
.map { DeviceEntity(id: $0.id, name: $0.name, mac: $0.mac) }
|
|
||||||
}
|
|
||||||
|
|
||||||
func suggestedEntities() async throws -> [DeviceEntity] {
|
|
||||||
let devices = SharedDeviceData.getDevices()
|
|
||||||
return devices.map { DeviceEntity(id: $0.id, name: $0.name, mac: $0.mac) }
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultResult() async -> DeviceEntity? {
|
|
||||||
let devices = SharedDeviceData.getDevices()
|
|
||||||
return devices.first.map { DeviceEntity(id: $0.id, name: $0.name, mac: $0.mac) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Widget Configuration Intent
|
|
||||||
|
|
||||||
struct ConfigurationAppIntent: WidgetConfigurationIntent {
|
|
||||||
static var title: LocalizedStringResource { "Device Control" }
|
|
||||||
static var description: IntentDescription { "Select a device to control from your home screen." }
|
|
||||||
|
|
||||||
@Parameter(title: "Device")
|
|
||||||
var device: DeviceEntity?
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 340 B |
|
Before Width: | Height: | Size: 703 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 503 B |
|
Before Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 703 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,122 +0,0 @@
|
|||||||
{
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"idiom": "iphone",
|
|
||||||
"size": "20x20",
|
|
||||||
"scale": "2x",
|
|
||||||
"filename": "App-Icon-20x20@2x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "iphone",
|
|
||||||
"size": "20x20",
|
|
||||||
"scale": "3x",
|
|
||||||
"filename": "App-Icon-20x20@3x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "iphone",
|
|
||||||
"size": "29x29",
|
|
||||||
"scale": "1x",
|
|
||||||
"filename": "App-Icon-29x29@1x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "iphone",
|
|
||||||
"size": "29x29",
|
|
||||||
"scale": "2x",
|
|
||||||
"filename": "App-Icon-29x29@2x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "iphone",
|
|
||||||
"size": "29x29",
|
|
||||||
"scale": "3x",
|
|
||||||
"filename": "App-Icon-29x29@3x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "iphone",
|
|
||||||
"size": "40x40",
|
|
||||||
"scale": "2x",
|
|
||||||
"filename": "App-Icon-40x40@2x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "iphone",
|
|
||||||
"size": "40x40",
|
|
||||||
"scale": "3x",
|
|
||||||
"filename": "App-Icon-40x40@3x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "iphone",
|
|
||||||
"size": "60x60",
|
|
||||||
"scale": "2x",
|
|
||||||
"filename": "App-Icon-60x60@2x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "iphone",
|
|
||||||
"size": "60x60",
|
|
||||||
"scale": "3x",
|
|
||||||
"filename": "App-Icon-60x60@3x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "ipad",
|
|
||||||
"size": "20x20",
|
|
||||||
"scale": "1x",
|
|
||||||
"filename": "App-Icon-20x20@1x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "ipad",
|
|
||||||
"size": "20x20",
|
|
||||||
"scale": "2x",
|
|
||||||
"filename": "App-Icon-20x20@2x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "ipad",
|
|
||||||
"size": "29x29",
|
|
||||||
"scale": "1x",
|
|
||||||
"filename": "App-Icon-29x29@1x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "ipad",
|
|
||||||
"size": "29x29",
|
|
||||||
"scale": "2x",
|
|
||||||
"filename": "App-Icon-29x29@2x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "ipad",
|
|
||||||
"size": "40x40",
|
|
||||||
"scale": "1x",
|
|
||||||
"filename": "App-Icon-40x40@1x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "ipad",
|
|
||||||
"size": "40x40",
|
|
||||||
"scale": "2x",
|
|
||||||
"filename": "App-Icon-40x40@2x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "ipad",
|
|
||||||
"size": "76x76",
|
|
||||||
"scale": "1x",
|
|
||||||
"filename": "App-Icon-76x76@1x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "ipad",
|
|
||||||
"size": "76x76",
|
|
||||||
"scale": "2x",
|
|
||||||
"filename": "App-Icon-76x76@2x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "ipad",
|
|
||||||
"size": "83.5x83.5",
|
|
||||||
"scale": "2x",
|
|
||||||
"filename": "App-Icon-83.5x83.5@2x.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "ios-marketing",
|
|
||||||
"size": "1024x1024",
|
|
||||||
"scale": "1x",
|
|
||||||
"filename": "ItunesArtwork@2x.png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info": {
|
|
||||||
"version": 1,
|
|
||||||
"author": "expo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 38 KiB |
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>NSExtension</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSExtensionPointIdentifier</key>
|
|
||||||
<string>com.apple.widgetkit-extension</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import AppIntents
|
|
||||||
import SwiftUI
|
|
||||||
import WidgetKit
|
|
||||||
|
|
||||||
// MARK: - Control Widget for Quick Wake Action
|
|
||||||
|
|
||||||
struct widgetControl: ControlWidget {
|
|
||||||
static let kind: String = "wakecontrol"
|
|
||||||
|
|
||||||
var body: some ControlWidgetConfiguration {
|
|
||||||
AppIntentControlConfiguration(
|
|
||||||
kind: Self.kind,
|
|
||||||
provider: WakeControlProvider()
|
|
||||||
) { value in
|
|
||||||
ControlWidgetButton(action: WakeDeviceIntent(deviceId: value.deviceId, deviceName: value.deviceName)) {
|
|
||||||
Label(value.deviceName, systemImage: "bolt.fill")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.displayName("Wake Device")
|
|
||||||
.description("Quickly wake a device from Control Center.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Control Widget Value
|
|
||||||
|
|
||||||
extension widgetControl {
|
|
||||||
struct Value {
|
|
||||||
var deviceId: String
|
|
||||||
var deviceName: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Control Widget Provider
|
|
||||||
|
|
||||||
struct WakeControlProvider: AppIntentControlValueProvider {
|
|
||||||
func previewValue(configuration: WakeControlConfiguration) -> widgetControl.Value {
|
|
||||||
widgetControl.Value(
|
|
||||||
deviceId: configuration.device?.id ?? "",
|
|
||||||
deviceName: configuration.device?.name ?? "Select Device"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func currentValue(configuration: WakeControlConfiguration) async throws -> widgetControl.Value {
|
|
||||||
widgetControl.Value(
|
|
||||||
deviceId: configuration.device?.id ?? "",
|
|
||||||
deviceName: configuration.device?.name ?? "Select Device"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Control Widget Configuration Intent
|
|
||||||
|
|
||||||
struct WakeControlConfiguration: ControlConfigurationIntent {
|
|
||||||
static let title: LocalizedStringResource = "Device to Wake"
|
|
||||||
|
|
||||||
@Parameter(title: "Device")
|
|
||||||
var device: DeviceEntity?
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Wake Device Intent (Opens Deep Link)
|
|
||||||
|
|
||||||
struct WakeDeviceIntent: AppIntent {
|
|
||||||
static var title: LocalizedStringResource = "Wake Device"
|
|
||||||
static var description = IntentDescription("Wakes the selected device using Wake-on-LAN.")
|
|
||||||
static var openAppWhenRun: Bool = true
|
|
||||||
static var isDiscoverable: Bool = true
|
|
||||||
|
|
||||||
@Parameter(title: "Device ID")
|
|
||||||
var deviceId: String
|
|
||||||
|
|
||||||
@Parameter(title: "Device Name")
|
|
||||||
var deviceName: String
|
|
||||||
|
|
||||||
// static var parameterSummary: some ParameterSummary {
|
|
||||||
// Summary("Wake \(\.$deviceName)")
|
|
||||||
// }
|
|
||||||
|
|
||||||
init() {
|
|
||||||
self.deviceId = ""
|
|
||||||
self.deviceName = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
init(deviceId: String, deviceName: String) {
|
|
||||||
self.deviceId = deviceId
|
|
||||||
self.deviceName = deviceName
|
|
||||||
}
|
|
||||||
|
|
||||||
func perform() async throws -> some IntentResult & OpensIntent {
|
|
||||||
print("WakeDeviceIntent performed for device: \(deviceName) (id: \(deviceId))")
|
|
||||||
let url = URL(string: "remotewol-upsnap://actions/wake/\(deviceId)")!
|
|
||||||
return .result(opensIntent: OpenURLIntent(url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
|
|
||||||
module.exports = config => ({
|
|
||||||
type: "widget",
|
|
||||||
icon: 'https://github.com/expo.png',
|
|
||||||
entitlements: {
|
|
||||||
"com.apple.security.application-groups": [
|
|
||||||
"group.abunchofknowitalls.remotewol-upsnap"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.application-groups</key>
|
|
||||||
<array>
|
|
||||||
<string>group.abunchofknowitalls.remotewol-upsnap</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import WidgetKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct exportWidgets: WidgetBundle {
|
|
||||||
var body: some Widget {
|
|
||||||
// Export widgets here
|
|
||||||
widget()
|
|
||||||
// widgetControl()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,66 +0,0 @@
|
|||||||
{
|
|
||||||
"fill" : {
|
|
||||||
"automatic-gradient" : "extended-gray:1.00000,1.00000"
|
|
||||||
},
|
|
||||||
"groups" : [
|
|
||||||
{
|
|
||||||
"layers" : [
|
|
||||||
{
|
|
||||||
"hidden" : false,
|
|
||||||
"image-name" : "Image.png",
|
|
||||||
"name" : "Image",
|
|
||||||
"position" : {
|
|
||||||
"scale" : 0.65,
|
|
||||||
"translation-in-points" : [
|
|
||||||
0,
|
|
||||||
-52.942187500000045
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fill-specializations" : [
|
|
||||||
{
|
|
||||||
"appearance" : "dark",
|
|
||||||
"value" : "none"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"image-name-specializations" : [
|
|
||||||
{
|
|
||||||
"value" : "display 2.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearance" : "dark",
|
|
||||||
"value" : "display.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearance" : "tinted",
|
|
||||||
"value" : "display 3.png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name" : "display 3",
|
|
||||||
"position" : {
|
|
||||||
"scale" : 1.25,
|
|
||||||
"translation-in-points" : [
|
|
||||||
0,
|
|
||||||
-10.625
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"shadow" : {
|
|
||||||
"kind" : "neutral",
|
|
||||||
"opacity" : 0.5
|
|
||||||
},
|
|
||||||
"translucency" : {
|
|
||||||
"enabled" : true,
|
|
||||||
"value" : 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"supported-platforms" : {
|
|
||||||
"circles" : [
|
|
||||||
"watchOS"
|
|
||||||
],
|
|
||||||
"squares" : "shared"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
import WidgetKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - Timeline Entry
|
|
||||||
|
|
||||||
struct DeviceEntry: TimelineEntry {
|
|
||||||
let date: Date
|
|
||||||
let configuration: ConfigurationAppIntent
|
|
||||||
let device: DeviceInfo?
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Timeline Provider
|
|
||||||
|
|
||||||
struct Provider: AppIntentTimelineProvider {
|
|
||||||
func placeholder(in context: Context) -> DeviceEntry {
|
|
||||||
DeviceEntry(
|
|
||||||
date: Date(),
|
|
||||||
configuration: ConfigurationAppIntent(),
|
|
||||||
device: DeviceInfo(id: "placeholder", name: "My Computer", mac: "AA:BB:CC:DD:EE:FF", ip: "192.168.1.100", canSleep: true, canShutdown: true, status: "unknown")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> DeviceEntry {
|
|
||||||
let device = configuration.device.flatMap { SharedDeviceData.getDevice(id: $0.id) }
|
|
||||||
return DeviceEntry(date: Date(), configuration: configuration, device: device)
|
|
||||||
}
|
|
||||||
|
|
||||||
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<DeviceEntry> {
|
|
||||||
let device = configuration.device.flatMap { SharedDeviceData.getDevice(id: $0.id) }
|
|
||||||
let entry = DeviceEntry(date: Date(), configuration: configuration, device: device)
|
|
||||||
|
|
||||||
// Widgets don't update frequently enough for status, so we just provide a static entry
|
|
||||||
// Refresh every 30 minutes to pick up device list changes
|
|
||||||
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
|
|
||||||
return Timeline(entries: [entry], policy: .after(nextUpdate))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Deep Link Helper
|
|
||||||
|
|
||||||
enum DeviceAction: String {
|
|
||||||
case wake
|
|
||||||
case sleep
|
|
||||||
case restart
|
|
||||||
case shutdown
|
|
||||||
|
|
||||||
func url(for deviceId: String) -> URL {
|
|
||||||
URL(string: "remotewol-upsnap://action/\(self.rawValue)/\(deviceId)")!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Widget View
|
|
||||||
|
|
||||||
struct DeviceWidgetEntryView: View {
|
|
||||||
var entry: Provider.Entry
|
|
||||||
@Environment(\.widgetFamily) var widgetFamily
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
if let device = entry.device {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
// Device Info Header
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(device.name)
|
|
||||||
.font(.system(size: 15, weight: .semibold))
|
|
||||||
.lineLimit(1)
|
|
||||||
Text(device.mac)
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(minLength: 4)
|
|
||||||
|
|
||||||
// Action Buttons
|
|
||||||
if widgetFamily == .systemSmall {
|
|
||||||
// Small widget: 2x2 grid
|
|
||||||
VStack(spacing: 6) {
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
ActionButton(action: .wake, deviceId: device.id, enabled: device.status == "offline")
|
|
||||||
ActionButton(action: .sleep, deviceId: device.id, enabled: device.status == "online" && device.canSleep)
|
|
||||||
}
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
ActionButton(action: .restart, deviceId: device.id, enabled: device.status == "online" && device.canShutdown)
|
|
||||||
ActionButton(action: .shutdown, deviceId: device.id, enabled: device.status == "online" && device.canShutdown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Medium/Large widget: horizontal layout
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
ActionButton(action: .wake, deviceId: device.id, enabled: device.status == "offline")
|
|
||||||
ActionButton(action: .sleep, deviceId: device.id, enabled: device.status == "online" && device.canSleep)
|
|
||||||
ActionButton(action: .restart, deviceId: device.id, enabled: device.status == "online" && device.canShutdown)
|
|
||||||
ActionButton(action: .shutdown, deviceId: device.id, enabled: device.status == "online" && device.canShutdown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
} else {
|
|
||||||
// No device selected
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Image(systemName: "desktopcomputer")
|
|
||||||
.font(.system(size: 32))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text("Select a Device")
|
|
||||||
.font(.system(size: 14, weight: .medium))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text("Long press to configure")
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary.opacity(0.7))
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Action Button
|
|
||||||
|
|
||||||
struct ActionButton: View {
|
|
||||||
let action: DeviceAction
|
|
||||||
let deviceId: String
|
|
||||||
let enabled: Bool
|
|
||||||
|
|
||||||
var icon: String {
|
|
||||||
switch action {
|
|
||||||
case .wake: return "bolt.fill"
|
|
||||||
case .sleep: return "moon.fill"
|
|
||||||
case .restart: return "arrow.clockwise"
|
|
||||||
case .shutdown: return "power"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var color: Color {
|
|
||||||
switch action {
|
|
||||||
case .wake: return .green
|
|
||||||
case .sleep: return .orange
|
|
||||||
case .restart: return .blue
|
|
||||||
case .shutdown: return .red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var disabled: Color {
|
|
||||||
return .gray
|
|
||||||
}
|
|
||||||
|
|
||||||
var label: String {
|
|
||||||
switch action {
|
|
||||||
case .wake: return "Wake"
|
|
||||||
case .sleep: return "Sleep"
|
|
||||||
case .restart: return "Restart"
|
|
||||||
case .shutdown: return "Shut Down"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Link(destination: enabled ? action.url(for: deviceId) : URL("")!) {
|
|
||||||
VStack(spacing: 2) {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.system(size: 16, weight: .medium))
|
|
||||||
.foregroundColor(enabled ? color : disabled)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, minHeight: 36)
|
|
||||||
.background((enabled ? color : disabled).opacity(0.15))
|
|
||||||
.cornerRadius(8)
|
|
||||||
.disabled(!enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Widget Definition
|
|
||||||
|
|
||||||
struct DeviceControlWidget: Widget {
|
|
||||||
let kind: String = "widget"
|
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
|
||||||
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
|
|
||||||
DeviceWidgetEntryView(entry: entry)
|
|
||||||
.containerBackground(.fill.tertiary, for: .widget)
|
|
||||||
}
|
|
||||||
.configurationDisplayName("Device Control")
|
|
||||||
.description("Control your devices with wake, sleep, restart, and shutdown actions.")
|
|
||||||
.supportedFamilies([.systemSmall, .systemMedium])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep legacy name for backwards compatibility with existing widget installations
|
|
||||||
typealias widget = DeviceControlWidget
|
|
||||||
|
|
||||||
// MARK: - Preview
|
|
||||||
|
|
||||||
#Preview(as: .systemSmall) {
|
|
||||||
DeviceControlWidget()
|
|
||||||
} timeline: {
|
|
||||||
DeviceEntry(
|
|
||||||
date: .now,
|
|
||||||
configuration: ConfigurationAppIntent(),
|
|
||||||
device: DeviceInfo(id: "preview", name: "Gaming PC", mac: "AA:BB:CC:DD:EE:FF", ip: "192.168.1.100", canSleep: true, canShutdown: true, status: "online")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview(as: .systemMedium) {
|
|
||||||
DeviceControlWidget()
|
|
||||||
} timeline: {
|
|
||||||
DeviceEntry(
|
|
||||||
date: .now,
|
|
||||||
configuration: ConfigurationAppIntent(),
|
|
||||||
device: DeviceInfo(id: "preview", name: "Home Server", mac: "11:22:33:44:55:66", ip: "192.168.1.50", canSleep: true, canShutdown: true, status: "offline")
|
|
||||||
)
|
|
||||||
}
|
|
||||||