Compare commits

...

10 Commits

Author SHA1 Message Date
Fin
4ba691ed3f 将图片渲染为图片链接 2025-11-24 16:56:27 +08:00
Fin
3463510e14 添加 markdown 2025-11-24 15:44:27 +08:00
Fin
862772d649 减少下载超时时间,预防被系统强制终止。 2025-11-24 14:45:21 +08:00
Neo
986a7fde4e 统一铃声音量大小, 缩减音频文件大小 2025-11-24 07:58:51 +08:00
Fin
995ab4c5ec 修复消息文本类型 2025-11-21 15:26:23 +08:00
Fin
98fade76fc 使用 Xcode 26.0 编译 2025-11-21 15:15:57 +08:00
Fin
15d2f62220 支持 markdown 2025-11-21 15:10:14 +08:00
Fin
4a2729a71e 更新 Podfile.lock 2025-11-20 17:20:53 +08:00
Fin
230e86df6c update docs 2025-11-19 16:14:15 +08:00
Fin
e617852fbb update docs 2025-11-19 16:04:54 +08:00
53 changed files with 589 additions and 64 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: '26.0'
- name: Setup ruby
uses: ruby/setup-ruby@v1

View File

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

View File

@ -121,6 +121,10 @@
069332222E6A8E3100F9387F /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0603706A20E20A7C00F4CA05 /* String+Extension.swift */; };
069332232E6A8E3100F9387F /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0603706A20E20A7C00F4CA05 /* String+Extension.swift */; };
0699473D2D223094008D5E40 /* CustomTapTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0699473C2D223094008D5E40 /* CustomTapTextView.swift */; };
069C6CA42ED03C72007244BB /* MarkdownProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 069C6CA32ED03C72007244BB /* MarkdownProcessor.swift */; };
069C6CA52ED03E10007244BB /* MarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C777CF2ECEFF210032A044 /* MarkdownParser.swift */; };
069C6CA72ED03E27007244BB /* Markdown in Frameworks */ = {isa = PBXBuildFile; productRef = 069C6CA62ED03E27007244BB /* Markdown */; };
069C6CA92ED03F07007244BB /* BKColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06840DBA272298FB001B3193 /* BKColor.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 */; };
@ -143,6 +147,8 @@
06C5952D2480E3F8006B98F3 /* LabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C5952C2480E3F8006B98F3 /* LabelCell.swift */; };
06C5953124811392006B98F3 /* ArchiveSettingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C5953024811392006B98F3 /* ArchiveSettingCell.swift */; };
06C595362481160F006B98F3 /* BKLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C595352481160F006B98F3 /* BKLabel.swift */; };
06C777D02ECEFF210032A044 /* MarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C777CF2ECEFF210032A044 /* MarkdownParser.swift */; };
06C777D32ECF05990032A044 /* Markdown in Frameworks */ = {isa = PBXBuildFile; productRef = 06C777D22ECF05990032A044 /* Markdown */; };
06CF784721C7A50300A052D7 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 06CF784021C7A50300A052D7 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
06CF784C21C7A51200A052D7 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CF784B21C7A51200A052D7 /* NotificationService.swift */; };
06D69E202C1159E200161A35 /* glass.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320500250B6DD3001561EC /* glass.caf */; };
@ -364,6 +370,7 @@
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>"; };
069C6CA32ED03C72007244BB /* MarkdownProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownProcessor.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>"; };
@ -384,6 +391,7 @@
06C5953024811392006B98F3 /* ArchiveSettingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveSettingCell.swift; sourceTree = "<group>"; };
06C5953224811505006B98F3 /* ArchiveSettingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveSettingManager.swift; sourceTree = "<group>"; };
06C595352481160F006B98F3 /* BKLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BKLabel.swift; sourceTree = "<group>"; };
06C777CF2ECEFF210032A044 /* MarkdownParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownParser.swift; sourceTree = "<group>"; };
06CF784021C7A50300A052D7 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
@ -444,6 +452,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
06C777D32ECF05990032A044 /* Markdown in Frameworks */,
06B1158D247BA6D5006D91FB /* CloudKit.framework in Frameworks */,
3428272069AFAFE2C683FEB0 /* libPods-Bark.a in Frameworks */,
);
@ -454,6 +463,7 @@
buildActionMask = 2147483647;
files = (
879AE4D4178855A9672009E4 /* libPods-NotificationServiceExtension.a in Frameworks */,
069C6CA72ED03E27007244BB /* Markdown in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -624,6 +634,7 @@
06E944742C07012E00AC86AB /* RealmConfiguration.swift */,
0687F2A72CCB791A00B2A52F /* UIFont+Extension.swift */,
06BCAE552CDB19260092867A /* GroupMuteSettingManager.swift */,
06C777CF2ECEFF210032A044 /* MarkdownParser.swift */,
);
path = Common;
sourceTree = "<group>";
@ -719,6 +730,7 @@
06D69E402C11983E00161A35 /* CallProcessor.swift */,
06E944792C0704E500AC86AB /* ImageDownloader.swift */,
06BCAE5A2CDB25120092867A /* MuteProcessor.swift */,
069C6CA32ED03C72007244BB /* MarkdownProcessor.swift */,
);
path = Processor;
sourceTree = "<group>";
@ -904,6 +916,9 @@
ja,
);
mainGroup = 0661A536204FDA4100965E4E;
packageReferences = (
06C777D12ECF05990032A044 /* XCRemoteSwiftPackageReference "swift-markdown" */,
);
productRefGroup = 0661A540204FDA4100965E4E /* Products */;
projectDirPath = "";
projectRoot = "";
@ -1253,6 +1268,7 @@
0642B55C27EB149900453D91 /* MutableTextCellViewModel.swift in Sources */,
062B98C8251B27AE004562E7 /* UINavigationItem+Extension.swift in Sources */,
060481EE250F404500BC9799 /* SoundsViewController.swift in Sources */,
06C777D02ECEFF210032A044 /* MarkdownParser.swift in Sources */,
0699473D2D223094008D5E40 /* CustomTapTextView.swift in Sources */,
06EEF333291CCFF400CA228A /* CryptoSettingController.swift in Sources */,
0603706D20E23EC000F4CA05 /* BarkSFSafariViewController.swift in Sources */,
@ -1335,14 +1351,17 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
069C6CA92ED03F07007244BB /* BKColor.swift in Sources */,
06E944752C07012E00AC86AB /* RealmConfiguration.swift in Sources */,
06E944732C06FF9200AC86AB /* ArchiveProcessor.swift in Sources */,
06E9447A2C0704E500AC86AB /* ImageDownloader.swift in Sources */,
06CF784C21C7A51200A052D7 /* NotificationService.swift in Sources */,
06FB04052C53575400F3A213 /* SharedDefines.swift in Sources */,
069C6CA42ED03C72007244BB /* MarkdownProcessor.swift in Sources */,
067AFB1D2E5D8BED00AE78E7 /* UNNotificationContent+Extension.swift in Sources */,
0653677629B719BC0038BDB8 /* CryptoSettingManager.swift in Sources */,
06E9446F2C06FF1E00AC86AB /* BadgeProcessor.swift in Sources */,
069C6CA52ED03E10007244BB /* MarkdownParser.swift in Sources */,
06BCAE582CDB19420092867A /* GroupMuteSettingManager.swift in Sources */,
06BCAE5B2CDB25120092867A /* MuteProcessor.swift in Sources */,
06E9446D2C06FEC900AC86AB /* LevelProcessor.swift in Sources */,
@ -1794,6 +1813,30 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
06C777D12ECF05990032A044 /* XCRemoteSwiftPackageReference "swift-markdown" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/swiftlang/swift-markdown";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.7.3;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
069C6CA62ED03E27007244BB /* Markdown */ = {
isa = XCSwiftPackageProductDependency;
package = 06C777D12ECF05990032A044 /* XCRemoteSwiftPackageReference "swift-markdown" */;
productName = Markdown;
};
06C777D22ECF05990032A044 /* Markdown */ = {
isa = XCSwiftPackageProductDependency;
package = 06C777D12ECF05990032A044 /* XCRemoteSwiftPackageReference "swift-markdown" */;
productName = Markdown;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 0661A537204FDA4100965E4E /* Project object */;
}

View File

@ -0,0 +1,24 @@
{
"originHash" : "bb354f5ee26f97a80bea96da70628d71c97b25e01e4e845afe4f5b1d0b6cf6e2",
"pins" : [
{
"identity" : "swift-cmark",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-cmark.git",
"state" : {
"revision" : "5d9bdaa4228b381639fff09403e39a04926e2dbe",
"version" : "0.7.1"
}
},
{
"identity" : "swift-markdown",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-markdown",
"state" : {
"revision" : "7d9a5ce307528578dfa777d505496bd5f544ad94",
"version" : "0.7.3"
}
}
],
"version" : 3
}

View File

@ -1885,6 +1885,35 @@
}
}
},
"image" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Image"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "画像"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "resim"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "图片"
}
}
}
},
"imageParameter" : {
"extractionState" : "manual",
"localizations" : {

346
Common/MarkdownParser.swift Normal file
View File

@ -0,0 +1,346 @@
//
// MarkdownParser.swift
// Bark
//
// Created by huangfeng on 2025/11/20.
// Copyright © 2025 Fin. All rights reserved.
//
import Markdown
import UIKit
class MarkdownParser {
struct Configuration {
let baseFont: UIFont
let baseColor: UIColor
let linkColor: UIColor
let codeTextColor: UIColor
let codeBackgroundColor: UIColor
let codeBlockTextColor: UIColor
let quoteColor: UIColor
init(
baseFont: UIFont = UIFont.preferredFont(ofSize: 14),
baseColor: UIColor = BKColor.grey.darken4,
linkColor: UIColor = BKColor.blue.base,
codeTextColor: UIColor = BKColor.blue.base,
codeBackgroundColor: UIColor = BKColor.grey.lighten4,
codeBlockTextColor: UIColor = BKColor.grey.darken4,
quoteColor: UIColor = BKColor.grey.base
) {
self.baseFont = baseFont
self.baseColor = baseColor
self.linkColor = linkColor
self.codeTextColor = codeTextColor
self.codeBackgroundColor = codeBackgroundColor
self.codeBlockTextColor = codeBlockTextColor
self.quoteColor = quoteColor
}
}
private let configuration: Configuration
init(configuration: Configuration = Configuration()) {
self.configuration = configuration
}
func parse(_ markdown: String) -> NSAttributedString {
let document = Document(parsing: markdown)
var walker = AttributedStringWalker(configuration: configuration)
walker.visit(document)
return walker.attributedString
}
}
private struct AttributedStringWalker: MarkupWalker {
let configuration: MarkdownParser.Configuration
let attributedString = NSMutableAttributedString()
var attributes: [NSAttributedString.Key: Any] = [:]
enum ListType {
case ordered
case unordered
}
var listStack: [(type: ListType, index: Int)] = []
init(configuration: MarkdownParser.Configuration) {
self.configuration = configuration
self.attributes = [
.font: configuration.baseFont,
.foregroundColor: configuration.baseColor
]
}
mutating func visitDocument(_ document: Document) {
for child in document.children {
visit(child)
}
}
///
mutating func visitParagraph(_ paragraph: Paragraph) {
//
//
if attributedString.length > 0, !(paragraph.parent is ListItem), !attributedString.string.hasSuffix("\n\n") {
attributedString.append(NSAttributedString(string: "\n\n", attributes: attributes))
}
for child in paragraph.children {
visit(child)
}
}
///
mutating func visitText(_ text: Text) {
attributedString.append(NSAttributedString(string: text.string, attributes: attributes))
}
///
mutating func visitStrong(_ strong: Strong) {
let originalAttributes = attributes
defer { attributes = originalAttributes }
let originalFont = attributes[.font] as? UIFont ?? configuration.baseFont
attributes[.font] = originalFont.bold()
for child in strong.children {
visit(child)
}
}
///
mutating func visitEmphasis(_ emphasis: Emphasis) {
let originalAttributes = attributes
defer { attributes = originalAttributes }
let originalFont = attributes[.font] as? UIFont ?? configuration.baseFont
attributes[.font] = originalFont.italic()
for child in emphasis.children {
visit(child)
}
}
/// 线
mutating func visitStrikethrough(_ strikethrough: Strikethrough) {
let originalAttributes = attributes
defer { attributes = originalAttributes }
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
for child in strikethrough.children {
visit(child)
}
}
///
mutating func visitLink(_ link: Link) {
let originalAttributes = attributes
defer { attributes = originalAttributes }
attributes[.foregroundColor] = configuration.linkColor
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
if let destination = link.destination {
attributes[.link] = destination
}
for child in link.children {
visit(child)
}
}
///
mutating func visitInlineCode(_ inlineCode: InlineCode) {
var currentAttributes = attributes
currentAttributes[.font] = UIFont.monospacedSystemFont(ofSize: configuration.baseFont.pointSize, weight: .regular)
currentAttributes[.foregroundColor] = configuration.codeTextColor
currentAttributes[.backgroundColor] = configuration.codeBackgroundColor
attributedString.append(NSAttributedString(string: inlineCode.code, attributes: currentAttributes))
}
///
mutating func visitCodeBlock(_ codeBlock: CodeBlock) {
if attributedString.length > 0, !attributedString.string.hasSuffix("\n\n") {
attributedString.append(NSAttributedString(string: "\n\n", attributes: attributes))
}
var currentAttributes = attributes
currentAttributes[.font] = UIFont.monospacedSystemFont(ofSize: configuration.baseFont.pointSize, weight: .regular)
currentAttributes[.foregroundColor] = configuration.codeBlockTextColor
currentAttributes[.backgroundColor] = configuration.codeBackgroundColor
attributedString.append(NSAttributedString(string: codeBlock.code, attributes: currentAttributes))
attributedString.append(NSAttributedString(string: "\n", attributes: [.font: configuration.baseFont]))
}
///
mutating func visitHeading(_ heading: Heading) {
if attributedString.length > 0, !attributedString.string.hasSuffix("\n\n") {
attributedString.append(NSAttributedString(string: "\n\n", attributes: attributes))
}
let originalAttributes = attributes
defer { attributes = originalAttributes }
let level = heading.level
var size = configuration.baseFont.pointSize
var weight = UIFont.Weight.bold
switch level {
case 1: size += 6; weight = .heavy
case 2: size += 4; weight = .bold
case 3: size += 2; weight = .semibold
default: break
}
attributes[.font] = UIFont.systemFont(ofSize: size, weight: weight)
for child in heading.children {
visit(child)
}
}
///
mutating func visitBlockQuote(_ blockQuote: BlockQuote) {
if attributedString.length > 0 {
attributedString.append(NSAttributedString(string: "\n", attributes: attributes))
}
let originalAttributes = attributes
defer { attributes = originalAttributes }
attributes[.foregroundColor] = configuration.quoteColor
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.firstLineHeadIndent = 10
paragraphStyle.headIndent = 10
attributes[.paragraphStyle] = paragraphStyle
for child in blockQuote.children {
visit(child)
}
}
///
mutating func visitOrderedList(_ orderedList: OrderedList) {
if attributedString.length > 0 && listStack.isEmpty {
attributedString.append(NSAttributedString(string: "\n", attributes: attributes))
}
listStack.append((type: .ordered, index: Int(orderedList.startIndex)))
for child in orderedList.children {
visit(child)
}
listStack.removeLast()
}
///
mutating func visitUnorderedList(_ unorderedList: UnorderedList) {
if attributedString.length > 0 && listStack.isEmpty {
attributedString.append(NSAttributedString(string: "\n", attributes: attributes))
}
listStack.append((type: .unordered, index: 0))
for child in unorderedList.children {
visit(child)
}
listStack.removeLast()
}
///
mutating func visitListItem(_ listItem: ListItem) {
let originalAttributes = attributes
defer { attributes = originalAttributes }
let level = CGFloat(listStack.count - 1)
let indent: CGFloat = 20
let baseIndent = level * indent
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.firstLineHeadIndent = baseIndent
paragraphStyle.headIndent = baseIndent + indent
paragraphStyle.paragraphSpacingBefore = 4
attributes[.paragraphStyle] = paragraphStyle
var prefix = ""
if let last = listStack.last {
if last.type == .ordered {
prefix = "\(last.index). "
} else if listItem.checkbox == nil {
prefix = ""
}
}
if attributedString.length > 0 {
attributedString.append(NSAttributedString(string: "\n", attributes: attributes))
}
attributedString.append(NSAttributedString(string: "\(prefix)", attributes: attributes))
if let checkbox = listItem.checkbox {
let imageName = checkbox == .checked ? "checkmark.square" : "square"
let font = attributes[.font] as? UIFont ?? configuration.baseFont
let color = attributes[.foregroundColor] as? UIColor ?? configuration.baseColor
let symbolConfiguration = UIImage.SymbolConfiguration(font: font)
if let image = UIImage(systemName: imageName, withConfiguration: symbolConfiguration)?.withTintColor(color, renderingMode: .alwaysOriginal) {
let attachment = NSTextAttachment()
attachment.image = image
let y = (font.capHeight - image.size.height).rounded() / 2
attachment.bounds = CGRect(x: 0, y: y, width: image.size.width, height: image.size.height)
attributedString.append(NSAttributedString(attachment: attachment))
attributedString.append(NSAttributedString(string: " ", attributes: attributes))
} else {
let text = checkbox == .checked ? "" : ""
attributedString.append(NSAttributedString(string: text, attributes: attributes))
}
}
for child in listItem.children {
visit(child)
}
// listStack
if !listStack.isEmpty {
var last = listStack[listStack.count - 1]
if last.type == .ordered {
last.index += 1
listStack[listStack.count - 1] = last
}
}
}
///
mutating func visitImage(_ image: Image) {
let originalAttributes = attributes
defer { attributes = originalAttributes }
attributes[.foregroundColor] = configuration.linkColor
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
if let source = image.source {
attributes[.link] = source
}
let altText = image.plainText.isEmpty ? "image".localized : image.plainText
attributedString.append(NSAttributedString(string: "[\(altText)]", attributes: attributes))
}
///
mutating func visitSoftBreak(_ softBreak: SoftBreak) {
attributedString.append(NSAttributedString(string: " ", attributes: attributes))
}
///
mutating func visitLineBreak(_ lineBreak: LineBreak) {
attributedString.append(NSAttributedString(string: "\n", attributes: attributes))
}
}

View File

@ -14,7 +14,7 @@ let kRealmDefaultConfiguration = {
let fileUrl = groupUrl?.appendingPathComponent("bark.realm")
let config = Realm.Configuration(
fileURL: fileUrl,
schemaVersion: 16,
schemaVersion: 17,
migrationBlock: { migration, oldSchemaVersion in
switch oldSchemaVersion {
case 0...13:

View File

@ -13,3 +13,19 @@ extension UIFont {
return UIFontMetrics.default.scaledFont(for: UIFont.systemFont(ofSize: size, weight: weight))
}
}
extension UIFont {
func bold() -> UIFont {
if let descriptor = self.fontDescriptor.withSymbolicTraits(.traitBold) {
return UIFont(descriptor: descriptor, size: self.pointSize)
}
return self
}
func italic() -> UIFont {
if let descriptor = self.fontDescriptor.withSymbolicTraits(.traitItalic) {
return UIFont(descriptor: descriptor, size: self.pointSize)
}
return self
}
}

View File

@ -11,21 +11,27 @@ import SwiftyJSON
import UIKit
class Message: Object {
@objc dynamic var id = NSUUID().uuidString
@objc dynamic var title: String?
@objc dynamic var subtitle: String?
@objc dynamic var body: String?
@objc dynamic var url: String?
@objc dynamic var image: String?
@objc dynamic var group: String?
@objc dynamic var createDate: Date?
override class func primaryKey() -> String? {
return "id"
enum BodyType: String {
case plainText
case markdown
}
@Persisted(primaryKey: true) var id = UUID().uuidString
@Persisted var title: String?
@Persisted var subtitle: String?
@Persisted var body: String?
@Persisted var bodyType: String?
@Persisted var url: String?
@Persisted var image: String?
@Persisted(indexed: true) var group: String?
@Persisted(indexed: true) var createDate: Date?
override class func indexedProperties() -> [String] {
return ["group", "createDate"]
var type: BodyType {
get {
guard let bodyType = bodyType else {
return .plainText
}
return BodyType(rawValue: bodyType) ?? .plainText
}
}
/// JSON
@ -41,6 +47,7 @@ class Message: Object {
self.title = json["title"].string
self.subtitle = json["subtitle"].string
self.body = json["body"].string
self.bodyType = json["bodyType"].string
self.url = json["url"].string
self.image = json["image"].string
self.group = json["group"].string

View File

@ -46,10 +46,15 @@ class MessageItemModel {
let body = message.body ?? ""
let url = message.url ?? ""
let text = NSMutableAttributedString(
string: body,
attributes: [.font: UIFont.preferredFont(ofSize: 14), .foregroundColor: BKColor.grey.darken4]
)
let text: NSMutableAttributedString
if message.type == .markdown {
text = NSMutableAttributedString(attributedString: MarkdownParser().parse(body))
} else {
text = NSMutableAttributedString(
string: body,
attributes: [.font: UIFont.preferredFont(ofSize: 14), .foregroundColor: BKColor.grey.darken4]
)
}
if subtitle.count > 0 {
// spacer

View File

@ -33,6 +33,7 @@ class NotificationService: UNNotificationServiceExtension {
.setIcon,
.setImage,
.mute,
.markdown,
.call
]

View File

@ -32,6 +32,7 @@ class ArchiveProcessor: NotificationContentProcessor {
let group = userInfo["group"] as? String
let image = userInfo["image"] as? String
let id = userInfo["id"] as? String
let markdown = userInfo["markdown"] as? String
try? realm?.write {
let message = Message()
@ -41,6 +42,10 @@ class ArchiveProcessor: NotificationContentProcessor {
message.title = title
message.subtitle = subtitle
message.body = body
if let markdown, !markdown.isEmpty {
message.body = markdown
message.bodyType = Message.BodyType.markdown.rawValue
}
message.url = url
message.image = image
message.group = group

View File

@ -28,7 +28,9 @@ class ImageDownloader {
/// - Returns: Result
class func downloadImage(url: URL) async -> Result<ImageLoadingResult, KingfisherError> {
return await withCheckedContinuation { continuation in
Kingfisher.ImageDownloader.default.downloadImage(with: url, options: nil) { result in
let downloader = Kingfisher.ImageDownloader.default
downloader.downloadTimeout = 10.0
downloader.downloadImage(with: url, options: nil) { result in
continuation.resume(returning: result)
}
}

View File

@ -0,0 +1,29 @@
//
// MarkdownProcessor.swift
// NotificationServiceExtension
//
// Created by huangfeng on 11/21/25.
// Copyright © 2025 Fin. All rights reserved.
//
import UIKit
class MarkdownProcessor: NotificationContentProcessor {
func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
let userInfo = bestAttemptContent.userInfo
guard let markdown = userInfo["markdown"] as? String, !markdown.isEmpty else {
return bestAttemptContent
}
let config = MarkdownParser.Configuration(
baseFont: UIFont.preferredFont(forTextStyle: .body),
baseColor: UIColor.white,
linkColor: UIColor.systemBlue,
codeTextColor: UIColor.black,
codeBackgroundColor: UIColor.gray,
codeBlockTextColor: UIColor.black,
quoteColor: UIColor.systemGray
)
bestAttemptContent.body = MarkdownParser(configuration: config).parse(markdown).string
return bestAttemptContent
}
}

View File

@ -19,6 +19,7 @@ enum NotificationContentProcessorItem {
case setImage
case call
case mute
case markdown
var processor: NotificationContentProcessor {
switch self {
@ -40,6 +41,8 @@ enum NotificationContentProcessorItem {
return CallProcessor()
case .mute:
return MuteProcessor()
case .markdown:
return MarkdownProcessor()
}
}
}

View File

@ -93,7 +93,7 @@ DEPENDENCIES:
- SwiftyStoreKit
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
trunk:
- Alamofire
- CryptoSwift
- DefaultsKit

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 KiB

View File

@ -1,46 +1,66 @@
#### Push usage limit <!-- {docsify-ignore-all} -->
Normal requests (HTTP status code 200) have no limit.<br>
But if more than 1000 error requests (HTTP status code 400 404 500) are made within 5 minutes, <b>the IP will be BAN for 24 hours</b>
### Unable to Receive Push Notifications
Check whether the Device Token is valid in the app settings.
If its valid, try rebooting your device. If you still cant receive notifications, check whether the push request returned HTTP 200.
#### Time-sensitive notifications not work
You can try to <b>restart your device</b> to solve it.
### DeviceToken Shows “Unknown”
This usually means the device cannot connect to Apples servers. You may also notice iMessage not working or other apps not receiving notifications.
Try switching networks, rebooting the device, or disabling any proxy/VPN affecting Apple services.
This is a connectivity issue between your device and Apples servers, and cannot be fixed by the app author.
#### Unable to save notification history, or unable to copy by pulling down push or swiping left on lock screen without clicking copy button
You can try to <b>restart your device</b> to solve it.<br />
Due to some reasons, the push service extension [UNNotificationServiceExtension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension) failed to run normally, and the code for saving notifications was not executed properly.
### Push Usage Limit
Valid requests (HTTP 200) have no limit.
If you send over **1000 error requests** (HTTP 400 / 404 / 500) within **5 minutes**, **your IP will be banned for 24 hours**.
#### Automatic copy push failure
After iOS 14.5 version, due to permission tightening, it is not possible to automatically copy push content to clipboard when receiving push. <br/>
You can temporarily pull down push or swipe left on lock screen and click view to automatically copy, or click copy button on pop-up push.
### Receiving Unknown or Unexpected Pushes (e.g., “NoContent”)
Possible causes:
1. Safari may auto-complete the Bark API URL when typing in the address bar and trigger preloading.
2. Chat apps like WeChat may periodically access a Bark API URL you sent earlier.
3. Your push key was leaked — reset it in the server list page.
#### Open notification history page by default
When you open APP again, it will jump to the last opened page.<br />
Just exit APP when you are on history message page. When you open APP again, it will be history message page.
### “Server Error” Prompt
Occasional errors may be ignored. The app might have gone into background causing network timeouts.
#### Does push API support POST request?
Bark supports GET POST , supports using Json <br>
No matter which request method, parameter names are the same. Refer to [usage tutorial](/en-us/tutorial)
### Time-Sensitive Notifications Not Working
Try **rebooting your device**.
#### Pushing special characters causes push failure. For example: Push content contains link or Push abnormal such as + becomes space
This is because of the problem of irregular link. It often happens<br>
When splicing URL, pay attention to URL encoding parameters
### Unable to Save Notification History or No Copy Button When Pulling Down Notification
Try **rebooting your device**.
The Notification Service Extension may have failed to run, so the saving logic didnt execute.
### Multiple Devices Using the Same Key but Only One Receives Notifications
A key can only be used by one device. Only the most recently opened app instance will receive notifications.
### Auto-Copy Not Working
On iOS 14.5+, stricter permissions prevent auto-copy when receiving notifications.
You can instead pull down the notification or swipe left on the lock screen to trigger auto-copy, or tap the copy button.
### Defaulting to Notification History on App Launch
The app reopens to the last viewed page.
If you exit the app on the history page, reopening it will return to the history page.
### Does the Push API Support POST Requests?
Bark supports both GET and POST, as well as JSON format.
Parameters are the same for all request types. See the tutorial for details.
### Push Fails Due to Special Characters (e.g., links, “+” becomes space)
This happens when the URL is not properly encoded.
```sh
# For example
https://api.day.app/key/{push content}
# Example
https://api.day.app/key/{content}
# If {push content} is
# If {content} is:
"a/b/c/"
# Then the final spliced URL is
# Final URL becomes:
https://api.day.app/key/a/b/c/
# The corresponding route will not be found and the backend program will return 404
# -> No route matches, backend returns 404
#You should url encode {push content} before splicing
# Correct (URL-encoded):
https://api.day.app/key/a%2Fb%2Fc%2F
```
If you use a mature HTTP library, parameters will be automatically processed and you dont need manual encoding. <br>
But if you splice URL yourself, you need special attention for special characters in parameters. **Its better not care whether there are special characters or not and blindly apply a layer of URL encoding.**
HTTP libraries usually encode parameters automatically.
If constructing URLs manually, always encode parameters.
#### How to ensure privacy and security
Refer [privacy security](/en-us/privacy)
See the [Privac](/en-us/privacy)

View File

@ -47,7 +47,7 @@ curl -X "POST" "https://api.day.app/your_key" \
curl -X "POST" "https://api.day.app/push" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"body": "Test Bark Server",
"markdown": "Hello **Markdown**",
"title": "Test Title",
"device_key": "your_key"
}'
@ -61,6 +61,7 @@ List of supported parameters, specific effects can be previewed in the APP.
| title | Push title |
| subtitle | Push subtitle |
| body | Push content |
| markdown | Push content in Markdown format. When this is provided, the body field is ignored. |
| device_key | Device key |
| device_keys | Key array, used for batch push |
| level | Push interruption level.<br>critical: Important alert, will ring even in silent mode <br>activeDefault value, the system will immediately light up the screen to display the notification<br>timeSensitiveTime-sensitive notification, can display the notification in focus mode.<br>passiveOnly adds the notification to the notification list, will not light up the screen. |

View File

@ -1,8 +1,3 @@
#### 江苏部分地区无法访问 https://api.day.app
江苏部分地区存在DNS污染和网络阻断预计会持续一到两周。<br/>
可以尝试更换DNS、翻墙或修改 hosts 添加一条记录 43.155.109.24 api.day.app。<br/>
如果无法解决,可暂时使用 https://api.bbark.top ,只需在发送端将 api.day.app 改为 api.bbark.top。<br/>api.bbark.top是临时域名随时可能终止解析请勿长期使用
#### 无法收到推送
在 App 设置中检查 Device Token 是否正常。如果不正常,参考 [这里](#DeviceToken显示未知)<br/>
如果正常,可以重启下设备,如果还不能接收到推送,检查推送请求返回状态码是否为 code 200。<br/>
@ -33,6 +28,9 @@
可以尝试<b>重启设备</b>来解决。<br />
因某些原因导致推送服务扩展([UNNotificationServiceExtension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension))未能正常运行,执行通知保存的代码未能正常执行。
#### 多台设备使用同一个key但只有其中一台设备可以收到推送
同一个Key只能一台设备使用只有最后打开的APP会收到推送
#### 自动复制推送失效
iOS 14.5 之后的版本因权限收紧,不能在收到推送时自动复制推送内容到剪切板。<br/>
可暂时先下拉推送或在锁屏界面左滑推送点查看即可自动复制,或点击弹出的推送复制按钮。

View File

@ -47,7 +47,7 @@ curl -X "POST" "https://api.day.app/your_key" \
curl -X "POST" "https://api.day.app/push" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"body": "Test Bark Server",
"markdown": "Hello **Markdown**",
"title": "Test Title",
"device_key": "your_key"
}'
@ -61,6 +61,7 @@ curl -X "POST" "https://api.day.app/push" \
| title | 推送标题 |
| subtitle | 推送副标题 |
| body | 推送内容 |
| markdown | 推送内容markdown格式。传递了此参数将忽略 body 字段, 发送时请注意处理特殊字符。|
| device_key | 设备key |
| device_keys | key 数组,用于批量推送 |
| level | 推送中断级别。<br>critical: 重要警告, 在静音模式下也会响铃 <br>active默认值系统会立即亮屏显示通知<br>timeSensitive时效性通知可在专注状态下显示通知。<br>passive仅将通知添加到通知列表不会亮屏提醒。 |
@ -87,9 +88,4 @@ curl -X "POST" "https://api.day.app/push" \
* [浏览器扩展](https://github.com/ij369/bark-sender) 将网页内容发送到手机
## 快捷指令
Bark 支持快捷指令直接发送推送,以下是当收到交警短信时,忽略静音模式持续响铃提醒用户的自动化示例。
<img src="../_media/shortcuts_cn.png" />
1. 创建个人自动化
2. 选择信息、填写信息包含关键词触发自动化,选择立即执行,点击下一步
3. 选择新建空白自动化,选择 Bark 发送推送到此设备快捷指令
4. 填写推送配置,标题可以选择短信发件人、内容可以选择短信内容,或自己自定义。
Bark 支持使用快捷指令直接发送推送