This repository was archived by the owner on Feb 24, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathQRSharingService.swift
182 lines (142 loc) · 5.37 KB
/
QRSharingService.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
//
// QRSharingService.swift
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import Foundation
import QuickLookUI
extension NSSharingService {
static let qrCode = QRSharingService()
}
final class QRSharingService: NSSharingService {
fileprivate var qrImage: NSImage?
fileprivate var imageUrl: URL?
fileprivate init() {
super.init(title: UserText.shareViaQRCodeMenuItem, image: .qrIcon, alternateImage: nil) {}
}
/// Get ASCII `Data` for an array of items to share that can be represented as strings (e.g., URLs or Strings).
private static func data(for items: [Any]?) -> Data? {
guard let items else { return nil }
for item in items {
var string: String? {
switch item {
case let url as URL:
return url.absoluteString
case let string as String:
return string
default:
return nil
}
}
if let data = string?.data(using: .nonLossyASCII) {
return data
}
}
return nil
}
override func canPerform(withItems items: [Any]?) -> Bool {
Self.data(for: items) != nil
}
private static func qrCode(for items: [Any]) -> CIImage? {
guard let data = Self.data(for: items) else { return nil }
let isDuckDuckGoURL = items.contains(where: { ($0 as? URL)?.isDuckDuckGo ?? false })
return CIImage.qrCode(for: data, parameters: isDuckDuckGoURL ? .duckDuckGo : .default)
}
override func perform(withItems items: [Any]) {
guard let qr = Self.qrCode(for: items) else { return }
let cgImage = qr.cgImage
guard let data = cgImage.bitmapRepresentation(using: .png) else { return }
// save to temp directory, will be removed on QLPreviewPanel hide
let fileUrl = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("png")
do {
try data.write(to: fileUrl)
} catch {
return
}
self.imageUrl = fileUrl
self.qrImage = NSImage(cgImage: cgImage, size: qr.extent.size.scaled(by: 1 / (NSScreen.main?.backingScaleFactor ?? NSScreen.defaultBackingScaleFactor)))
self.showQuickLook()
}
private func showQuickLook() {
guard let qlPanel = QLPreviewPanel.shared() else { return }
if !qlPanel.isVisible {
qlPanel.makeKeyAndOrderFront(nil)
}
qlPanel.updateController()
}
fileprivate func cleanup() {
guard let imageUrl else { return }
if let qlPanel = QLPreviewPanel.shared(),
qlPanel.delegate === self,
qlPanel.isVisible {
qlPanel.orderOut(nil)
}
try? FileManager.default.removeItem(at: imageUrl)
self.imageUrl = nil
self.qrImage = nil
}
}
extension QRSharingService: QLPreviewPanelDataSource, QLPreviewPanelDelegate {
func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
return imageUrl != nil ? 1 : 0
}
func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! {
return imageUrl as QLPreviewItem?
}
func previewPanel(_ panel: QLPreviewPanel!, sourceFrameOnScreenFor item: QLPreviewItem!) -> NSRect {
return qrImage.map { NSRect(origin: .zero, size: $0.size) } ?? .zero
}
func previewPanel(_ panel: QLPreviewPanel!, transitionImageFor item: QLPreviewItem!, contentRect: UnsafeMutablePointer<NSRect>!) -> Any! {
return qrImage
}
}
extension NSView {
open override func acceptsPreviewPanelControl(_ panel: QLPreviewPanel!) -> Bool {
NSSharingService.qrCode.imageUrl != nil && NSSharingService.qrCode.qrImage != nil || panel.isVisible
}
open override func beginPreviewPanelControl(_ qlPanel: QLPreviewPanel!) {
let qrCode = NSSharingService.qrCode
guard qrCode.imageUrl != nil && qrCode.qrImage != nil else {
qlPanel.closeIfNeeded()
return
}
qlPanel.dataSource = qrCode
qlPanel.delegate = qrCode
qlPanel.reloadData()
}
open override func endPreviewPanelControl(_ qlPanel: QLPreviewPanel!) {
qlPanel.dataSource = nil
qlPanel.delegate = nil
qlPanel.reloadData()
qlPanel.closeIfNeeded()
NSSharingService.qrCode.cleanup()
}
}
private extension QLPreviewPanel {
private static var isClosing = false
func closeIfNeeded() {
guard !Self.isClosing else { return }
guard isVisible else {
Self.isClosing = false
return
}
Self.isClosing = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
Self.isClosing = false
}
self.close()
}
}