| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- import Foundation
- public struct PromptCandidate: Equatable, Sendable {
- public var hostProcess: String
- public var texts: [String]
- public var buttonTitles: [String]
- public init(hostProcess: String, texts: [String], buttonTitles: [String]) {
- self.hostProcess = hostProcess
- self.texts = texts
- self.buttonTitles = buttonTitles
- }
- }
- public struct AirPlayPromptMatch: Equatable, Sendable {
- public var summary: String
- public var deviceName: String?
- public var acceptButtonTitle: String
- public var declineButtonTitle: String?
- public init(
- summary: String,
- deviceName: String?,
- acceptButtonTitle: String,
- declineButtonTitle: String?
- ) {
- self.summary = summary
- self.deviceName = deviceName
- self.acceptButtonTitle = acceptButtonTitle
- self.declineButtonTitle = declineButtonTitle
- }
- }
- public enum AirPlayPromptMatcher {
- public static let acceptTitles = [
- "accept", "allow", "ok", "接受", "允許", "好"
- ]
- public static let declineTitles = [
- "decline", "deny", "don't allow", "do not allow", "cancel", "拒絕", "不允許", "取消"
- ]
- private static let airPlayTerms = [
- "airplay", "air play", "隔空播放", "螢幕鏡像", "屏幕镜像"
- ]
- private static let requestPatterns = [
- #"would\s+like\s+to\s+airplay\s+to\s+this\s+mac"#,
- #"wants\s+to\s+airplay\s+to\s+this\s+mac"#,
- #"is\s+trying\s+to\s+airplay\s+to\s+this\s+mac"#,
- #"想要.{0,40}(?:airplay|隔空播放|螢幕鏡像|屏幕镜像).{0,40}(?:這台|这台|此)\s*mac"#,
- #"正在(?:嘗試|尝试).{0,40}(?:airplay|隔空播放|螢幕鏡像|屏幕镜像).{0,40}(?:這台|这台|此)\s*mac"#
- ]
- public static func match(_ candidate: PromptCandidate) -> AirPlayPromptMatch? {
- let texts = candidate.texts
- .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
- .filter { !$0.isEmpty }
- let combinedText = texts.joined(separator: " ")
- let normalizedText = normalize(combinedText)
- guard airPlayTerms.contains(where: { normalizedText.contains($0) }) else {
- return nil
- }
- guard isAirPlayConnectionRequest(combinedText) else {
- return nil
- }
- guard let acceptTitle = candidate.buttonTitles.first(where: isAcceptTitle) else {
- return nil
- }
- return AirPlayPromptMatch(
- summary: summarize(combinedText),
- deviceName: extractDeviceName(from: combinedText),
- acceptButtonTitle: acceptTitle,
- declineButtonTitle: candidate.buttonTitles.first(where: isDeclineTitle)
- )
- }
- public static func isAcceptTitle(_ title: String) -> Bool {
- let normalizedTitle = normalize(title)
- return acceptTitles.contains(normalizedTitle)
- }
- public static func isDeclineTitle(_ title: String) -> Bool {
- let normalizedTitle = normalize(title)
- return declineTitles.contains(normalizedTitle)
- }
- public static func extractDeviceName(from text: String) -> String? {
- let patterns = [
- #"["“](.+?)["”]\s+(?:would like|wants|is trying).{0,80}AirPlay"#,
- #"(.+?)\s+(?:would like|wants|is trying).{0,80}AirPlay"#,
- #"(?:Allow|Accept)\s+["“](.+?)["”].{0,80}AirPlay"#,
- #"AirPlay.{0,80}(?:from|for)\s+["“](.+?)["”]"#,
- #"["「](.+?)["」]\s*(?:想要|正在嘗試|正在尝试).{0,80}(?:AirPlay|隔空播放|螢幕鏡像|屏幕镜像)"#,
- #"(?:接受|允許)\s*["「](.+?)["」].{0,80}(?:AirPlay|隔空播放|螢幕鏡像|屏幕镜像)"#
- ]
- for pattern in patterns {
- if let match = firstCapture(in: text, pattern: pattern) {
- let cleaned = cleanupDeviceName(match)
- if !cleaned.isEmpty {
- return cleaned
- }
- }
- }
- return nil
- }
- private static func firstCapture(in text: String, pattern: String) -> String? {
- guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else {
- return nil
- }
- let range = NSRange(text.startIndex..<text.endIndex, in: text)
- guard
- let match = regex.firstMatch(in: text, options: [], range: range),
- match.numberOfRanges > 1,
- let captureRange = Range(match.range(at: 1), in: text)
- else {
- return nil
- }
- return String(text[captureRange])
- }
- private static func isAirPlayConnectionRequest(_ text: String) -> Bool {
- requestPatterns.contains { pattern in
- guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else {
- return false
- }
- let range = NSRange(text.startIndex..<text.endIndex, in: text)
- return regex.firstMatch(in: text, options: [], range: range) != nil
- }
- }
- private static func cleanupDeviceName(_ value: String) -> String {
- value
- .replacingOccurrences(of: "\n", with: " ")
- .replacingOccurrences(of: " ", with: " ")
- .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines.union(.punctuationCharacters))
- }
- private static func summarize(_ text: String) -> String {
- let cleaned = text
- .replacingOccurrences(of: "\n", with: " ")
- .replacingOccurrences(of: " ", with: " ")
- .trimmingCharacters(in: .whitespacesAndNewlines)
- if cleaned.count <= 240 {
- return cleaned
- }
- return String(cleaned.prefix(237)) + "..."
- }
- private static func normalize(_ text: String) -> String {
- text
- .trimmingCharacters(in: .whitespacesAndNewlines)
- .folding(options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive], locale: .current)
- .lowercased()
- }
- }
|