diff --git a/Bark.xcodeproj/project.pbxproj b/Bark.xcodeproj/project.pbxproj index d25bea1..89b9fb6 100644 --- a/Bark.xcodeproj/project.pbxproj +++ b/Bark.xcodeproj/project.pbxproj @@ -179,6 +179,7 @@ 06D69E3E2C1159E200161A35 /* alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F3250B6DD1001561EC /* alarm.caf */; }; 06D69E3F2C1159E200161A35 /* fanfare.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FB250B6DD2001561EC /* fanfare.caf */; }; 06D69E412C11983E00161A35 /* CallProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06D69E402C11983E00161A35 /* CallProcessor.swift */; }; + 06E62C112D670F62004DC82B /* PushToCurrentIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E62C102D670F5D004DC82B /* PushToCurrentIntent.swift */; }; 06E944682C06E40600AC86AB /* NotificationContentProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */; }; 06E9446A2C06E4A200AC86AB /* CiphertextProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */; }; 06E9446D2C06FEC900AC86AB /* LevelProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */; }; @@ -193,6 +194,9 @@ 06EE1FD326843E9300586708 /* BarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EE1FD226843E9300586708 /* BarkTests.swift */; }; 06EEF333291CCFF400CA228A /* CryptoSettingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EEF332291CCFF400CA228A /* CryptoSettingController.swift */; }; 06EEF335291CD00000CA228A /* CryptoSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EEF334291CD00000CA228A /* CryptoSettingViewModel.swift */; }; + 06EF49152D682A99008B91D2 /* PushResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EF49142D682A96008B91D2 /* PushResponse.swift */; }; + 06EF49172D682AC4008B91D2 /* OptionsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EF49162D682AC3008B91D2 /* OptionsProvider.swift */; }; + 06EF49192D682B3B008B91D2 /* PushToOtherIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EF49182D682B34008B91D2 /* PushToOtherIntent.swift */; }; 06F08EA429B098DD006AB9CA /* CryptoSettingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F08EA329B098DD006AB9CA /* CryptoSettingManager.swift */; }; 06F08EA529B1DDA7006AB9CA /* Algorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061894C629A75BEA00E001C2 /* Algorithm.swift */; }; 06F08EA729B1DDFE006AB9CA /* Error+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F08EA629B1DDFE006AB9CA /* Error+Extension.swift */; }; @@ -391,6 +395,7 @@ 06CF784421C7A50300A052D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 06CF784B21C7A51200A052D7 /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 06D69E402C11983E00161A35 /* CallProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallProcessor.swift; sourceTree = ""; }; + 06E62C102D670F5D004DC82B /* PushToCurrentIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushToCurrentIntent.swift; sourceTree = ""; }; 06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentProcessor.swift; sourceTree = ""; }; 06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CiphertextProcessor.swift; sourceTree = ""; }; 06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LevelProcessor.swift; sourceTree = ""; }; @@ -406,6 +411,9 @@ 06EE1FD426843E9300586708 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 06EEF332291CCFF400CA228A /* CryptoSettingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSettingController.swift; sourceTree = ""; }; 06EEF334291CD00000CA228A /* CryptoSettingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSettingViewModel.swift; sourceTree = ""; }; + 06EF49142D682A96008B91D2 /* PushResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushResponse.swift; sourceTree = ""; }; + 06EF49162D682AC3008B91D2 /* OptionsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsProvider.swift; sourceTree = ""; }; + 06EF49182D682B34008B91D2 /* PushToOtherIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushToOtherIntent.swift; sourceTree = ""; }; 06F08EA329B098DD006AB9CA /* CryptoSettingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSettingManager.swift; sourceTree = ""; }; 06F08EA629B1DDFE006AB9CA /* Error+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Extension.swift"; sourceTree = ""; }; 06F08EAB29B1DECD006AB9CA /* NSLocalizedString+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLocalizedString+Extension.swift"; sourceTree = ""; }; @@ -675,6 +683,7 @@ 0661A541204FDA4100965E4E /* Bark */ = { isa = PBXGroup; children = ( + 06EF49132D682A82008B91D2 /* Intents */, 0683486A2050F1310024B6DA /* Bark.entitlements */, 0661A542204FDA4100965E4E /* AppDelegate.swift */, 0661A549204FDA4100965E4E /* Assets.xcassets */, @@ -738,6 +747,17 @@ path = BarkTests; sourceTree = ""; }; + 06EF49132D682A82008B91D2 /* Intents */ = { + isa = PBXGroup; + children = ( + 06EF49162D682AC3008B91D2 /* OptionsProvider.swift */, + 06EF49142D682A96008B91D2 /* PushResponse.swift */, + 06E62C102D670F5D004DC82B /* PushToCurrentIntent.swift */, + 06EF49182D682B34008B91D2 /* PushToOtherIntent.swift */, + ); + path = Intents; + sourceTree = ""; + }; 99BD309BDB7F62B5DC0CECE1 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1239,6 +1259,7 @@ 06AE311C266F54A500B39FBB /* GroupTableViewCell.swift in Sources */, 0672CB06256903F700570C9D /* MessageListViewModel.swift in Sources */, 0627DABD2990D615002F3F69 /* BorderTextField.swift in Sources */, + 06E62C112D670F62004DC82B /* PushToCurrentIntent.swift in Sources */, 0633E80A256A091B00ED0680 /* MJRefresh+Rx.swift in Sources */, 0637FA8C20E0D7A700E80174 /* BaseViewController.swift in Sources */, 0642B55C27EB149900453D91 /* MutableTextCellViewModel.swift in Sources */, @@ -1286,6 +1307,7 @@ 0604F7DF20620D4900B32F09 /* ServerManager.swift in Sources */, 0667D192247D162C005DE2ED /* MessageTableViewCell.swift in Sources */, 06E944762C07013000AC86AB /* RealmConfiguration.swift in Sources */, + 06EF49152D682A99008B91D2 /* PushResponse.swift in Sources */, 0603706720E1E31600F4CA05 /* Defines.swift in Sources */, 06787C3B2AB82BDB008ABDD7 /* CrashReportViewController.swift in Sources */, 064CAB9E256BE9090018155C /* PreviewCardCellViewModel.swift in Sources */, @@ -1308,6 +1330,7 @@ 06EEF335291CD00000CA228A /* CryptoSettingViewModel.swift in Sources */, 0637FA8020E0981E00E80174 /* BarkSettings.swift in Sources */, 065BE4402563D649002A8CA4 /* SoundsViewModel.swift in Sources */, + 06EF49192D682B3B008B91D2 /* PushToOtherIntent.swift in Sources */, 0647DB682CE604AA00102066 /* MessageSettingFooter.swift in Sources */, 065BE4462563D7E5002A8CA4 /* ViewModelType.swift in Sources */, 0603706B20E20A7C00F4CA05 /* String+Extension.swift in Sources */, @@ -1318,6 +1341,7 @@ 06BBB8B72567AC140076F63E /* MessageSettingsViewModel.swift in Sources */, 06BBB8C12567B3EF0076F63E /* BaseTableViewCell.swift in Sources */, 0661A543204FDA4100965E4E /* AppDelegate.swift in Sources */, + 06EF49172D682AC4008B91D2 /* OptionsProvider.swift in Sources */, 06F08EAF29B5D9FF006AB9CA /* HUD.swift in Sources */, 065AE76B2987777F00323230 /* ArchiveSettingRelay.swift in Sources */, 06F08EAC29B1DECD006AB9CA /* NSLocalizedString+Extension.swift in Sources */, diff --git a/Bark/Intents/OptionsProvider.swift b/Bark/Intents/OptionsProvider.swift new file mode 100644 index 0000000..33ef1c9 --- /dev/null +++ b/Bark/Intents/OptionsProvider.swift @@ -0,0 +1,64 @@ +// +// OptionsProvider.swift +// Bark +// +// Created by huangfeng on 2/21/25. +// Copyright © 2025 Fin. All rights reserved. +// +import AppIntents + +@available(iOS 16, *) +struct ServerAddressOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + return ServerManager.shared.servers.map { server in + return server.address + "/" + server.key + } + } + + func defaultResult() async -> String? { + return ServerManager.shared.currentServer.address + "/" + ServerManager.shared.currentServer.key + } +} + +@available(iOS 16, *) +struct SoundOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + var customSounds: [String] = [] + if let soundsDirectoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")?.appendingPathComponent("Library/Sounds").path { + customSounds = getSounds(urls: getFilesInDirectory(directory: soundsDirectoryUrl, suffix: "caf")) + } + let defaultSounds = getSounds(urls: Bundle.main.urls(forResourcesWithExtension: "caf", subdirectory: nil) ?? []) + + return customSounds + defaultSounds + } + + func getSounds(urls: [URL]) -> [String] { + let urls = urls.sorted { u1, u2 -> Bool in + u1.lastPathComponent.localizedStandardCompare(u2.lastPathComponent) == ComparisonResult.orderedAscending + } + return urls.map { $0.deletingPathExtension().lastPathComponent } + } + + func getFilesInDirectory(directory: String, suffix: String) -> [URL] { + let fileManager = FileManager.default + do { + let files = try fileManager.contentsOfDirectory(atPath: directory) + return files.compactMap { file -> URL? in + if file.hasSuffix(suffix), !file.hasPrefix(kBarkSoundPrefix) { + // 不要包含 kBarkSoundPrefix 开头的,这些是为了 call=1 合成的 30s 长铃声,不算用户上传的 + return URL(fileURLWithPath: directory).appendingPathComponent(file) + } + return nil + } + } catch { + return [] + } + } +} + +@available(iOS 16, *) +struct VolumeOptionsProvider: DynamicOptionsProvider { + func results() async throws -> [Int] { + return Array(0...10) + } +} diff --git a/Bark/Intents/PushResponse.swift b/Bark/Intents/PushResponse.swift new file mode 100644 index 0000000..b53619c --- /dev/null +++ b/Bark/Intents/PushResponse.swift @@ -0,0 +1,12 @@ +// +// PushResponse.swift +// Bark +// +// Created by huangfeng on 2/21/25. +// Copyright © 2025 Fin. All rights reserved. +// + +struct PushResponse: Decodable { + let message: String? + let code: Int? +} diff --git a/Bark/Intents/PushToCurrentIntent.swift b/Bark/Intents/PushToCurrentIntent.swift new file mode 100644 index 0000000..23962ec --- /dev/null +++ b/Bark/Intents/PushToCurrentIntent.swift @@ -0,0 +1,86 @@ +// +// Intents.swift +// Bark +// +// Created by huangfeng on 2/20/25. +// Copyright © 2025 Fin. All rights reserved. +// + +import Alamofire +import AppIntents + +@available(iOS 16, *) +struct PushToCurrentIntent: AppIntent { + static var title: LocalizedStringResource = "sendPushNotification" + static var openAppWhenRun: Bool = false + + @Parameter(title: "ServerAddress", optionsProvider: ServerAddressOptionsProvider()) + var address: String + + @Parameter(title: "CustomedNotificationTitle") + var title: String? + @Parameter(title: "CustomedNotificationContent") + var body: String? + + @Parameter(title: "ringtone") + var isCall: Bool + + @Parameter(title: "criticalAlert") + var isCritical: Bool + + @Parameter(title: "ringtoneVolume", optionsProvider: VolumeOptionsProvider()) + var volume: Int? + + @Parameter(title: "notificationSound", optionsProvider: SoundOptionsProvider()) + var sound: String? + + @Parameter(title: "notificationIcon") + var icon: URL? + + @Parameter(title: "group") + var group: String? + + func perform() async throws -> some IntentResult & ReturnsValue { + let url = ServerManager.shared.currentServer.address + "/\(ServerManager.shared.currentServer.key)" + + var params: [String: Any] = [:] + + if let title { + params["title"] = title + } + if let body { + params["body"] = body + } + if title == nil, body == nil { + params["body"] = "Empty Notification" + } + if isCritical { + params["level"] = "critical" + } + if let volume { + params["volume"] = volume + } + if isCall { + params["call"] = 1 + } + if let sound { + params["sound"] = sound + } + if let icon { + params["icon"] = icon.absoluteString + } + if let group { + params["group"] = group + } + + let response = await AF.request(url, method: .post, parameters: params, encoding: JSONEncoding.default) + .serializingDecodable(PushResponse.self) + .response + + // 打印返回的body + if response.response?.statusCode != 200 { + return .result(value: false) + } + return .result(value: true) + } +} diff --git a/Bark/Intents/PushToOtherIntent.swift b/Bark/Intents/PushToOtherIntent.swift new file mode 100644 index 0000000..ae27306 --- /dev/null +++ b/Bark/Intents/PushToOtherIntent.swift @@ -0,0 +1,85 @@ +// +// PushToOtherIntent.swift +// Bark +// +// Created by huangfeng on 2/21/25. +// Copyright © 2025 Fin. All rights reserved. +// +import Alamofire +import AppIntents + +@available(iOS 16, *) +struct PushToOtherIntent: AppIntent { + static var title: LocalizedStringResource = "sendPushNotificationToOther" + static var openAppWhenRun: Bool = false + + @Parameter(title: "ServerAddress") + var address: String + + @Parameter(title: "CustomedNotificationTitle") + var title: String? + @Parameter(title: "CustomedNotificationContent") + var body: String? + + @Parameter(title: "ringtone") + var isCall: Bool + + @Parameter(title: "criticalAlert") + var isCritical: Bool + + @Parameter(title: "ringtoneVolume", optionsProvider: VolumeOptionsProvider()) + var volume: Int? + + @Parameter(title: "notificationSound", optionsProvider: SoundOptionsProvider()) + var sound: String? + + @Parameter(title: "notificationIcon") + var icon: URL? + + @Parameter(title: "group") + var group: String? + + func perform() async throws -> some IntentResult & ReturnsValue { + let url = ServerManager.shared.currentServer.address + "/\(ServerManager.shared.currentServer.key)" + + var params: [String: Any] = [:] + + if let title { + params["title"] = title + } + if let body { + params["body"] = body + } + if title == nil, body == nil { + params["body"] = "Empty Notification" + } + if isCritical { + params["level"] = "critical" + } + if let volume { + params["volume"] = volume + } + if isCall { + params["call"] = 1 + } + if let sound { + params["sound"] = sound + } + if let icon { + params["icon"] = icon.absoluteString + } + if let group { + params["group"] = group + } + + let response = await AF.request(url, method: .post, parameters: params, encoding: JSONEncoding.default) + .serializingDecodable(PushResponse.self) + .response + + // 打印返回的body + if response.response?.statusCode != 200 { + return .result(value: false) + } + return .result(value: true) + } +} diff --git a/Bark/Localizable.xcstrings b/Bark/Localizable.xcstrings index d52328f..cf127c3 100644 --- a/Bark/Localizable.xcstrings +++ b/Bark/Localizable.xcstrings @@ -3306,6 +3306,35 @@ } } }, + "ringtoneVolume" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ringtone Volume for Critical Alerts" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "重要な警告の着信音量" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önemli Uyarılar için Zil Sesi Seviyesi" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重要警告铃声音量" + } + } + } + }, "SecureConnection" : { "extractionState" : "manual", "localizations" : { @@ -3335,6 +3364,62 @@ } } }, + "sendPushNotification" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Send notification to this device" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスにプッシュ通知を送信" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu cihaza bir bildirim gönder" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "发送推送到此设备" + } + } + } + }, + "sendPushNotificationToOther" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Send notification to other device" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "他のデバイスにプッシュ通知を送信する" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diğer cihazlara bildirim gönder" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "发送推送到其他设备" + } + } + } + }, "ServerAddress" : { "extractionState" : "manual", "localizations" : {