Compare commits

...

8 Commits

Author SHA1 Message Date
Fin
7a9a70eb25 修复解析旧数据失败的问题 2025-02-24 16:37:51 +08:00
Fin
355e2e7cb0 设置服务器名称 2025-02-24 15:42:34 +08:00
Fin
2bc0eb33b8 修复发送到其他设备快捷指令异常的问题 2025-02-24 09:51:23 +08:00
Fin
786e5f2232 url解码快捷指令参数 2025-02-21 18:33:34 +08:00
Fin
13b1a198d1 添加快捷指令教程 2025-02-21 14:47:30 +08:00
Fin
accf1008e7 移除无用代码 2025-02-21 14:15:08 +08:00
Feng
f56bfe82b6
Update testflight.yaml 2025-02-20 19:51:03 -08:00
Fin
c1a9e0aa78 支持使用快捷指令直接发送推送 2025-02-21 11:41:19 +08:00
18 changed files with 448 additions and 318 deletions

View File

@ -23,7 +23,7 @@ jobs:
- name: Select Xcode Version
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 'latest-stable'
xcode-version: '16.1'
- name: Setup ruby
uses: ruby/setup-ruby@v1

View File

@ -118,10 +118,6 @@
068EC15A27ED99E700D5D11E /* ServerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068EC15927ED99E700D5D11E /* ServerListViewModel.swift */; };
068F66B3247BD84C00DAD25A /* MessageListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068F66B2247BD84C00DAD25A /* MessageListViewController.swift */; };
0699473D2D223094008D5E40 /* CustomTapTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0699473C2D223094008D5E40 /* CustomTapTextView.swift */; };
06AE3118266F4E2E00B39FBB /* GroupFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AE3117266F4E2E00B39FBB /* GroupFilterViewController.swift */; };
06AE311A266F4E6600B39FBB /* GroupFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AE3119266F4E6600B39FBB /* GroupFilterViewModel.swift */; };
06AE311C266F54A500B39FBB /* GroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AE311B266F54A500B39FBB /* GroupTableViewCell.swift */; };
06AE311E266F54CC00B39FBB /* GroupCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AE311D266F54CC00B39FBB /* GroupCellViewModel.swift */; };
06B1158D247BA6D5006D91FB /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06B1158C247BA6D5006D91FB /* CloudKit.framework */; };
06B1158F247BB1FB006D91FB /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B1158E247BB1FB006D91FB /* Message.swift */; };
06B11591247BC132006D91FB /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B1158E247BB1FB006D91FB /* Message.swift */; };
@ -179,6 +175,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 +190,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 */; };
@ -363,10 +363,6 @@
068EC15927ED99E700D5D11E /* ServerListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListViewModel.swift; sourceTree = "<group>"; };
068F66B2247BD84C00DAD25A /* MessageListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListViewController.swift; sourceTree = "<group>"; };
0699473C2D223094008D5E40 /* CustomTapTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTapTextView.swift; sourceTree = "<group>"; };
06AE3117266F4E2E00B39FBB /* GroupFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupFilterViewController.swift; sourceTree = "<group>"; };
06AE3119266F4E6600B39FBB /* GroupFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupFilterViewModel.swift; sourceTree = "<group>"; };
06AE311B266F54A500B39FBB /* GroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupTableViewCell.swift; sourceTree = "<group>"; };
06AE311D266F54CC00B39FBB /* GroupCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCellViewModel.swift; sourceTree = "<group>"; };
06B1158C247BA6D5006D91FB /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
06B1158E247BB1FB006D91FB /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
06B11590247BBC15006D91FB /* NotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationServiceExtension.entitlements; sourceTree = "<group>"; };
@ -391,6 +387,7 @@
06CF784421C7A50300A052D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
06CF784B21C7A51200A052D7 /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
06D69E402C11983E00161A35 /* CallProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallProcessor.swift; sourceTree = "<group>"; };
06E62C102D670F5D004DC82B /* PushToCurrentIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushToCurrentIntent.swift; sourceTree = "<group>"; };
06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentProcessor.swift; sourceTree = "<group>"; };
06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CiphertextProcessor.swift; sourceTree = "<group>"; };
06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LevelProcessor.swift; sourceTree = "<group>"; };
@ -406,6 +403,9 @@
06EE1FD426843E9300586708 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
06EEF332291CCFF400CA228A /* CryptoSettingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSettingController.swift; sourceTree = "<group>"; };
06EEF334291CD00000CA228A /* CryptoSettingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSettingViewModel.swift; sourceTree = "<group>"; };
06EF49142D682A96008B91D2 /* PushResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushResponse.swift; sourceTree = "<group>"; };
06EF49162D682AC3008B91D2 /* OptionsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsProvider.swift; sourceTree = "<group>"; };
06EF49182D682B34008B91D2 /* PushToOtherIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushToOtherIntent.swift; sourceTree = "<group>"; };
06F08EA329B098DD006AB9CA /* CryptoSettingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSettingManager.swift; sourceTree = "<group>"; };
06F08EA629B1DDFE006AB9CA /* Error+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Extension.swift"; sourceTree = "<group>"; };
06F08EAB29B1DECD006AB9CA /* NSLocalizedString+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLocalizedString+Extension.swift"; sourceTree = "<group>"; };
@ -484,8 +484,6 @@
06BBB8B62567AC140076F63E /* MessageSettingsViewModel.swift */,
060481ED250F404500BC9799 /* SoundsViewController.swift */,
065BE43F2563D649002A8CA4 /* SoundsViewModel.swift */,
06AE3117266F4E2E00B39FBB /* GroupFilterViewController.swift */,
06AE3119266F4E6600B39FBB /* GroupFilterViewModel.swift */,
06F11E7627D9D5FB00F00298 /* QRScannerViewController.swift */,
068EC15727ED99C900D5D11E /* ServerListViewController.swift */,
068EC15927ED99E700D5D11E /* ServerListViewModel.swift */,
@ -517,8 +515,6 @@
062B98C2251B2762004562E7 /* BKButton.swift */,
06C595352481160F006B98F3 /* BKLabel.swift */,
062B98C7251B27AE004562E7 /* UINavigationItem+Extension.swift */,
06AE311B266F54A500B39FBB /* GroupTableViewCell.swift */,
06AE311D266F54CC00B39FBB /* GroupCellViewModel.swift */,
06C2CF222685B88D0034B127 /* TextCell.swift */,
06C2CF242685BDB80034B127 /* SpacerCell.swift */,
0642B55927EB13F100453D91 /* MutableTextCell.swift */,
@ -675,6 +671,7 @@
0661A541204FDA4100965E4E /* Bark */ = {
isa = PBXGroup;
children = (
06EF49132D682A82008B91D2 /* Intents */,
0683486A2050F1310024B6DA /* Bark.entitlements */,
0661A542204FDA4100965E4E /* AppDelegate.swift */,
0661A549204FDA4100965E4E /* Assets.xcassets */,
@ -738,6 +735,17 @@
path = BarkTests;
sourceTree = "<group>";
};
06EF49132D682A82008B91D2 /* Intents */ = {
isa = PBXGroup;
children = (
06EF49162D682AC3008B91D2 /* OptionsProvider.swift */,
06EF49142D682A96008B91D2 /* PushResponse.swift */,
06E62C102D670F5D004DC82B /* PushToCurrentIntent.swift */,
06EF49182D682B34008B91D2 /* PushToOtherIntent.swift */,
);
path = Intents;
sourceTree = "<group>";
};
99BD309BDB7F62B5DC0CECE1 /* Frameworks */ = {
isa = PBXGroup;
children = (
@ -1236,9 +1244,9 @@
0603706920E1F89500F4CA05 /* PreviewCardCell.swift in Sources */,
066890082D1946D500E106F2 /* MessageItemView.swift in Sources */,
0627DABB298B6EA2002F3F69 /* DropBoxView.swift in Sources */,
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 */,
@ -1247,7 +1255,6 @@
0699473D2D223094008D5E40 /* CustomTapTextView.swift in Sources */,
06EEF333291CCFF400CA228A /* CryptoSettingController.swift in Sources */,
0603706D20E23EC000F4CA05 /* BarkSFSafariViewController.swift in Sources */,
06AE311A266F4E6600B39FBB /* GroupFilterViewModel.swift in Sources */,
06F08EA429B098DD006AB9CA /* CryptoSettingManager.swift in Sources */,
0668900B2D19525400E106F2 /* ShowLessAndClearView.swift in Sources */,
06BD4DAA2901352E003364DB /* Object+Dictionary.swift in Sources */,
@ -1274,18 +1281,17 @@
06C5952F248107F5006B98F3 /* iCloudStatusCell.swift in Sources */,
06BBB88725650C6C0076F63E /* ArchiveSettingManager.swift in Sources */,
065BE44B2563D8E1002A8CA4 /* Reusable.swift in Sources */,
06AE311E266F54CC00B39FBB /* GroupCellViewModel.swift in Sources */,
0637FA8620E0AB6600E80174 /* UIColor+Extension.swift in Sources */,
0637FA8A20E0D58800E80174 /* NewServerViewController.swift in Sources */,
067B2EB525693E38008B6BE1 /* MessageSection.swift in Sources */,
0637FA8220E09C4B00E80174 /* BarkNavigationController.swift in Sources */,
06AE3118266F4E2E00B39FBB /* GroupFilterViewController.swift in Sources */,
0637FA7A20E092B300E80174 /* Observable+Extension.swift in Sources */,
060481F0250F51CA00BC9799 /* SoundCell.swift in Sources */,
0667D194247D1BA0005DE2ED /* Date+Extension.swift in Sources */,
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 +1314,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 +1325,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 */,

View File

@ -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)
}
}

View File

@ -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?
}

View File

@ -0,0 +1,88 @@
//
// 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<Bool> {
guard let address = URL(string: address) else {
throw "Invalid URL"
}
var params: [String: Any] = [:]
if let title, !title.isEmpty {
params["title"] = title.urlDecoded()
}
if let body, !body.isEmpty {
params["body"] = body.urlDecoded()
}
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, !sound.isEmpty {
params["sound"] = sound
}
if let icon {
params["icon"] = icon.absoluteString
}
if let group, !group.isEmpty {
params["group"] = group
}
let response = await AF.request(address, 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)
}
}

View File

@ -0,0 +1,87 @@
//
// 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<Bool> {
guard let address = URL(string: address) else {
throw "Invalid URL"
}
var params: [String: Any] = [:]
if let title, !title.isEmpty {
params["title"] = title.urlDecoded()
}
if let body, !body.isEmpty {
params["body"] = body.urlDecoded()
}
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, !sound.isEmpty {
params["sound"] = sound
}
if let icon {
params["icon"] = icon.absoluteString
}
if let group, !group.isEmpty {
params["group"] = group
}
let response = await AF.request(address, 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)
}
}

View File

@ -1920,7 +1920,7 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Groups"
"value" : "Group"
}
},
"ja" : {
@ -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" : {
@ -3509,6 +3594,35 @@
}
}
},
"setServerName" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Set Server Name"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "サーバー名を設定"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sunucu Adını Ayarla"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "设置服务器名称"
}
}
}
},
"setSounds" : {
"extractionState" : "manual",
"localizations" : {

View File

@ -17,6 +17,7 @@ class Server: Codable {
let address: String
var key: String
var state: Client.ClienState
var name: String?
var host: String {
return URL(string: address)?.host ?? ""
@ -33,6 +34,7 @@ class Server: Codable {
case id
case address
case key
case name
}
//
@ -41,6 +43,7 @@ class Server: Codable {
id = try container.decode(String.self, forKey: .id)
address = try container.decode(String.self, forKey: .address)
key = try container.decode(String.self, forKey: .key)
name = try? container.decode(String?.self, forKey: .name)
state = .ok
}
}
@ -182,4 +185,9 @@ class ServerManager: NSObject {
self.saveServers()
}
}
func setServerName(server: Server, name: String?) {
server.name = name
saveServers()
}
}

View File

@ -1,101 +0,0 @@
//
// GroupFilterViewController.swift
// Bark
//
// Created by huangfeng on 2021/6/8.
// Copyright © 2021 Fin. All rights reserved.
//
import Material
import MJRefresh
import RealmSwift
import RxCocoa
import RxDataSources
import RxSwift
import UIKit
class GroupFilterViewController: BaseViewController<GroupFilterViewModel> {
let doneButton: BKButton = {
let btn = BKButton()
btn.setTitle(NSLocalizedString("done"), for: .normal)
btn.setTitleColor(BKColor.lightBlue.darken3, for: .normal)
btn.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
btn.fontSize = 14
return btn
}()
let showAllGroupsButton: BKButton = {
let btn = BKButton()
btn.setTitle(NSLocalizedString("hideAllGroups"), for: .selected)
btn.setTitle(NSLocalizedString("showAllGroups"), for: .normal)
btn.setTitleColor(Color.lightBlue.darken3, for: .normal)
btn.fontSize = 14
return btn
}()
let tableView: UITableView = {
let tableView = UITableView(frame: CGRect.zero, style: .insetGrouped)
tableView.separatorStyle = .singleLine
tableView.separatorColor = BKColor.grey.lighten3
tableView.backgroundColor = BKColor.background.primary
tableView.register(GroupTableViewCell.self, forCellReuseIdentifier: "\(GroupTableViewCell.self)")
return tableView
}()
override func makeUI() {
self.title = NSLocalizedString("group")
self.navigationItem.setRightBarButtonItem(item: UIBarButtonItem(customView: doneButton))
self.view.addSubview(tableView)
self.view.addSubview(showAllGroupsButton)
tableView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.bottom.equalToSuperview().offset((kSafeAreaInsets.bottom + 40) * -1)
make.left.right.equalToSuperview()
}
showAllGroupsButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.height.equalTo(40)
make.bottom.equalToSuperview().offset(-kSafeAreaInsets.bottom)
}
self.tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: 20))
}
override func bindViewModel() {
let output = viewModel.transform(
input: GroupFilterViewModel.Input(
showAllGroups: self.showAllGroupsButton.rx
.tap
.compactMap { [weak self] in
guard let strongSelf = self else { return nil }
return !strongSelf.showAllGroupsButton.isSelected
}
.asDriver(onErrorDriveWith: .empty()),
doneTap: self.doneButton.rx.tap.asDriver()
))
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, GroupCellViewModel>> { _, tableView, _, item -> UITableViewCell in
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(GroupTableViewCell.self)") as? GroupTableViewCell else {
return UITableViewCell()
}
cell.bindViewModel(model: item)
return cell
}
output.groups
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: rx.disposeBag)
output.isShowAllGroups
.drive(self.showAllGroupsButton.rx.isSelected)
.disposed(by: rx.disposeBag)
output.dismiss.drive(onNext: { [weak self] in
self?.dismiss(animated: true, completion: nil)
})
.disposed(by: rx.disposeBag)
}
}
extension GroupFilterViewController: UITableViewDelegate {}

View File

@ -1,91 +0,0 @@
//
// GroupFilterViewModel.swift
// Bark
//
// Created by huangfeng on 2021/6/8.
// Copyright © 2021 Fin. All rights reserved.
//
import RealmSwift
import RxCocoa
import RxDataSources
import RxSwift
struct GroupFilterModel {
var name: String?
var checked: Bool
}
class GroupFilterViewModel: ViewModel, ViewModelType {
let groups: [GroupFilterModel]
init(groups: [GroupFilterModel]) {
self.groups = groups
}
struct Input {
var showAllGroups: Driver<Bool>
var doneTap: Driver<Void>
}
struct Output {
var groups: Driver<[SectionModel<String, GroupCellViewModel>]>
var isShowAllGroups: Driver<Bool>
var dismiss: Driver<Void>
}
var done = PublishRelay<[String?]>()
func transform(input: Input) -> Output {
// cellModel
let groupCellModels = self.groups.map { filterModel in
GroupCellViewModel(groupFilterModel: filterModel)
}
// cell checked
input.showAllGroups.drive(onNext: { isShowAllGroups in
for model in groupCellModels {
model.checked.accept(isShowAllGroups)
}
}).disposed(by: rx.disposeBag)
// cell checked
let checkChanged = Observable.merge(groupCellModels.map { model in
model.checked.asObservable()
})
//
let isShowAllGroups =
checkChanged
.map { _ in
groupCellModels.filter { viewModel in
viewModel.checked.value
}.count >= groupCellModels.count
}
input.doneTap.map { () -> [String?] in
let isShowAllGroups = groupCellModels.filter { viewModel in
viewModel.checked.value
}.count >= groupCellModels.count
if isShowAllGroups {
return []
}
return groupCellModels
.filter { $0.checked.value }
.map { $0.name.value }
}
.asObservable()
.bind(to: self.done)
.disposed(by: rx.disposeBag)
let dismiss = PublishRelay<Void>()
input.doneTap.map { _ in () }
.asObservable()
.bind(to: dismiss)
.disposed(by: rx.disposeBag)
return Output(
groups: Driver.just([SectionModel(model: "header", items: groupCellModels)]),
isShowAllGroups: isShowAllGroups.asDriver(onErrorDriveWith: .empty()),
dismiss: dismiss.asDriver(onErrorDriveWith: .empty())
)
}
}

View File

@ -17,6 +17,7 @@ enum ServerActionType {
case copy
case reset(key: String?)
case delete
case setName(name: String?)
}
func == (lhs: ServerActionType, rhs: ServerActionType) -> Bool {
@ -90,12 +91,21 @@ class ServerListViewController: BaseViewController<ServerListViewModel> {
}
return nil
}.asDriver(onErrorDriveWith: .empty())
//
let setServerName = action.compactMap { r -> (Server, String?)? in
if case ServerActionType.setName(let name) = r.1 {
return (r.0, name)
}
return nil
}.asDriver(onErrorDriveWith: .empty())
let output = viewModel.transform(input: ServerListViewModel.Input(
selectServer: selectServer,
copyServer: copyServer,
deleteServer: deleteServer,
resetServer: resetServer
resetServer: resetServer,
setServerName: setServerName
))
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, ServerListTableViewCellViewModel>> { _, tableView, _, item -> UITableViewCell in
@ -136,7 +146,7 @@ class ServerListViewController: BaseViewController<ServerListViewModel> {
return relay
}
let alertController = UIAlertController(title: nil, message: "\(viewModel.address.value)", preferredStyle: .actionSheet)
let alertController = UIAlertController(title: nil, message: "\(URL(string: viewModel.server.address)?.host ?? "")", preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction(title: NSLocalizedString("copyAddressAndKey"), style: .default, handler: { _ in
relay.accept((viewModel.server, .copy))
}))
@ -157,6 +167,18 @@ class ServerListViewController: BaseViewController<ServerListViewModel> {
relay.accept((viewModel.server, .select))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("setServerName"), style: .default, handler: { _ in
let alertController = UIAlertController(title: NSLocalizedString("setServerName"), message: nil, preferredStyle: .alert)
alertController.addTextField { textField in
textField.text = viewModel.server.name
}
alertController.addAction(UIAlertAction(title: NSLocalizedString("confirm"), style: .default, handler: { _ in
relay.accept((viewModel.server, .setName(name: alertController.textFields?.first?.text)))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel"), style: .cancel, handler: nil))
self.navigationController?.present(alertController, animated: true, completion: nil)
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("deleteServer"), style: .destructive, handler: { _ in
let alertController = UIAlertController(title: nil, message: NSLocalizedString("confirmDeleteServer"), preferredStyle: .alert)

View File

@ -19,6 +19,7 @@ class ServerListViewModel: ViewModel, ViewModelType {
let copyServer: Driver<Server>
let deleteServer: Driver<Server>
let resetServer: Driver<(Server, String?)>
let setServerName: Driver<(Server, String?)>
}
struct Output {
@ -37,6 +38,11 @@ class ServerListViewModel: ViewModel, ViewModelType {
let copy = input.copyServer.map { server -> String in
"\(server.address)/\(server.key)/"
}
//
input.setServerName.drive(onNext: { server, name in
ServerManager.shared.setServerName(server: server, name: name)
}).disposed(by: rx.disposeBag)
//
let deleteCheck = input.deleteServer.map { server -> Server? in
@ -134,7 +140,8 @@ class ServerListViewModel: ViewModel, ViewModelType {
.merge(
Observable.just(()),
serverDeleted,
serverResetSuccess
serverResetSuccess,
input.setServerName.map { _ in () }.asObservable()
)
.map {
[SectionModel(

View File

@ -1,22 +0,0 @@
//
// GroupCellViewModel.swift
// Bark
//
// Created by huangfeng on 2021/6/8.
// Copyright © 2021 Fin. All rights reserved.
//
import RxCocoa
import RxDataSources
import RxSwift
import UIKit
class GroupCellViewModel: ViewModel {
let name = BehaviorRelay<String?>(value: nil)
let checked = BehaviorRelay<Bool>(value: false)
init(groupFilterModel: GroupFilterModel) {
self.name.accept(groupFilterModel.name)
self.checked.accept(groupFilterModel.checked)
}
}

View File

@ -1,78 +0,0 @@
//
// GroupTableViewCell.swift
// Bark
//
// Created by huangfeng on 2021/6/8.
// Copyright © 2021 Fin. All rights reserved.
//
import Material
import UIKit
class GroupTableViewCell: BaseTableViewCell<GroupCellViewModel> {
let nameLabel: UILabel = {
let label = UILabel()
label.fontSize = 14
label.textColor = BKColor.grey.darken4
return label
}()
let checkButton: BKButton = {
let btn = BKButton()
btn.setImage(UIImage(named: "baseline_radio_button_unchecked_black_24pt"), for: .normal)
btn.setImage(UIImage(named: "baseline_check_circle_outline_black_24pt"), for: .selected)
btn.tintColor = BKColor.grey.base
btn.isUserInteractionEnabled = false
return btn
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
self.backgroundColor = BKColor.background.secondary
self.contentView.addSubview(nameLabel)
self.contentView.addSubview(checkButton)
checkButton.snp.makeConstraints { make in
make.width.height.equalTo(24)
make.left.equalToSuperview().offset(15)
make.centerY.equalToSuperview()
}
nameLabel.snp.makeConstraints { make in
make.left.equalTo(checkButton.snp.right).offset(15)
make.top.equalToSuperview().offset(15)
make.bottom.equalToSuperview().offset(-15)
}
let tap = UITapGestureRecognizer()
self.contentView.addGestureRecognizer(tap)
tap.rx.event.subscribe(onNext: { [weak self] _ in
self?.viewModel?.checked.accept(!self!.checkButton.isSelected)
}).disposed(by: rx.disposeBag)
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func bindViewModel(model: GroupCellViewModel) {
super.bindViewModel(model: model)
model.name
.map { name in
name ?? NSLocalizedString("default")
}
.bind(to: nameLabel.rx.text)
.disposed(by: rx.reuseBag)
model.checked
.bind(to: self.checkButton.rx.isSelected)
.disposed(by: rx.reuseBag)
model.checked.subscribe(
onNext: { [weak self] checked in
self?.checkButton.tintColor = checked ? BKColor.lightBlue.darken3 : BKColor.grey.base
}).disposed(by: rx.reuseBag)
}
}

View File

@ -101,7 +101,7 @@ class ServerListTableViewCell: BaseTableViewCell<ServerListTableViewCellViewMode
override func bindViewModel(model: ServerListTableViewCellViewModel) {
super.bindViewModel(model: model)
model.address
model.name
.bind(to: addressLabel.rx.text)
.disposed(by: rx.reuseBag)

View File

@ -12,15 +12,19 @@ import UIKit
class ServerListTableViewCellViewModel: ViewModel {
let server: Server
let address: BehaviorRelay<String>
let name: BehaviorRelay<String>
let key: BehaviorRelay<String>
let state: BehaviorRelay<Bool>
init(server: Server) {
self.server = server
self.address = BehaviorRelay<String>(value: {
URL(string: server.address)?.host ?? "Invalid Server"
self.name = BehaviorRelay<String>(value: {
var serverName = URL(string: server.address)?.host ?? "Invalid Server"
if let name = server.name, !name.isEmpty {
serverName = name + "\n" + serverName
}
return serverName
}())
self.key = BehaviorRelay<String>(value: !server.key.isEmpty ? server.key : "none")
self.state = BehaviorRelay<Bool>(value: server.state == .ok)

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 KiB

View File

@ -75,4 +75,12 @@ curl -X "POST" "https://api.day.app/push" \
| ciphertext | 加密推送的密文 |
| isArchive | 传 1 保存推送传其他的不保存推送不传按APP内设置来决定是否保存。 |
| url | 点击推送时跳转的URL 支持URL Scheme 和 Universal Link |
| action | 传 "none" 时,点击推送不会弹窗 |
| action | 传 "none" 时,点击推送不会弹窗 |
## 快捷指令
Bark 支持快捷指令直接发送推送,以下是当收到交警短信时,忽略静音模式持续响铃提醒用户的自动化示例。
<img src="../_media/shortcuts_cn.png" />
1. 创建个人自动化
2. 选择信息、填写信息包含关键词触发自动化,选择立即执行,点击下一步
3. 选择新建空白自动化,选择 Bark 发送推送到此设备快捷指令
4. 填写推送配置,标题可以选择短信发件人、内容可以选择短信内容,或自己自定义。