Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd9462d9f5 | ||
|
|
238769656a | ||
|
|
fd29254e38 | ||
|
|
6b0e14de3e | ||
|
|
db70d40e43 | ||
|
|
4394b78242 | ||
|
|
488f1a14b2 | ||
|
|
72bfd88161 | ||
|
|
2f6b0ff5ba | ||
|
|
5fdc5f8a1b | ||
|
|
ee091f60cd | ||
|
|
6c00c0f321 | ||
|
|
65a0ab3226 | ||
|
|
f5ca60a0ad | ||
|
|
ce16bca415 | ||
|
|
e6d1c7b1a3 | ||
|
|
84d2150893 | ||
|
|
007784c483 | ||
|
|
78cf2309f9 | ||
|
|
976fc280db | ||
|
|
02ab04c69e | ||
|
|
fcbe99dd85 | ||
|
|
feaf557ca2 | ||
|
|
9a488a2ae2 | ||
|
|
80b81ae228 | ||
|
|
0112e512c0 | ||
|
|
0d0b720114 | ||
|
|
13f3073077 | ||
|
|
ff39ba4c7d | ||
|
|
245358ae96 | ||
|
|
429069f372 | ||
|
|
04e056da02 | ||
|
|
50d4d4ee92 | ||
|
|
86b7e4ca24 | ||
|
|
d3a95c068f | ||
|
|
e3cbc19f11 | ||
|
|
c46f6eedbc | ||
|
|
ea213906d6 | ||
|
|
4700a9c56e | ||
|
|
4fe30da52b | ||
|
|
bd93961c59 | ||
|
|
f244572508 | ||
|
|
d4ea143d51 | ||
|
|
2dbc46f792 | ||
|
|
f61be69507 | ||
|
|
82f40d780f | ||
|
|
606082d76d | ||
|
|
2233699ece | ||
|
|
130935c01d | ||
|
|
74226d2fc3 | ||
|
|
e895e7a68e | ||
|
|
843ed9fe4f | ||
|
|
2ef3bc3b44 | ||
|
|
216e2cf851 | ||
|
|
d63665136b | ||
|
|
42c101605f | ||
|
|
fd1dfb4f4a | ||
|
|
767a5c5706 | ||
|
|
8e78f58b5e | ||
|
|
935bdc5a65 | ||
|
|
f559a6ac9d | ||
|
|
e853ba2b36 | ||
|
|
4621395a87 | ||
|
|
55e2a72a90 | ||
|
|
159e83179b | ||
|
|
6a77e4aba6 | ||
|
|
ffe3ac7a4a | ||
|
|
2dbe945f3e | ||
|
|
e828edd6ae | ||
|
|
7fbe254118 | ||
|
|
cffaabc076 | ||
|
|
3736605509 | ||
|
|
e0407d9398 | ||
|
|
3ed93e6079 | ||
|
|
e1924e1919 | ||
|
|
c10facbedd | ||
|
|
a1b4a075a6 | ||
|
|
bf7ad37d15 | ||
|
|
e68167788a | ||
|
|
48eb5be4a5 | ||
|
|
a352e9ed1c | ||
|
|
0f8c9d254e | ||
|
|
be4d342894 | ||
|
|
811b991fcd | ||
|
|
d46b62d3de | ||
|
|
093390084f | ||
|
|
b16556dd1b | ||
|
|
f5d895ed56 | ||
|
|
c489ef018f | ||
|
|
7e0a2e2830 | ||
|
|
0b94a888b5 | ||
|
|
15677b7843 | ||
|
|
6401c1969c | ||
|
|
bb1f28af0b | ||
|
|
d367e0ff34 | ||
|
|
ec093cf302 | ||
|
|
edc09b048c | ||
|
|
9f15ee85f6 | ||
|
|
10ef0c5a21 | ||
|
|
0aa4fd6894 | ||
|
|
71bddf6541 | ||
|
|
3ac2280dc6 | ||
|
|
8b965a7eb2 | ||
|
|
f11943475c | ||
|
|
a5781e6e96 | ||
|
|
ed20c9155e | ||
|
|
b55d6ff07b | ||
|
|
c86ed97bc2 | ||
|
|
eb32603c5f | ||
|
|
044753efb1 | ||
|
|
e8fdd0e94a | ||
|
|
09ef75d249 | ||
|
|
3551c1a006 | ||
|
|
d748167f0e | ||
|
|
02e8af22ab | ||
|
|
0e1fab8faa | ||
|
|
0396c56a31 | ||
|
|
ffdf667cc6 | ||
|
|
cce84e9460 | ||
|
|
b546370e87 | ||
|
|
fbd14c2280 | ||
|
|
bf1a8164d9 | ||
|
|
0a53ad79cb | ||
|
|
800fd23be3 |
No files matched your search
@@ -1,3 +1,4 @@
|
|||||||
DerivedData/
|
DerivedData/
|
||||||
|
.DerivedData/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 1019 KiB |
|
After Width: | Height: | Size: 850 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 777 KiB |
@@ -0,0 +1,220 @@
|
|||||||
|
{
|
||||||
|
"forceMultiLine" : true,
|
||||||
|
"screenshots" : [
|
||||||
|
{
|
||||||
|
"baseText" : "Quick Transfer to Android & more.",
|
||||||
|
"color" : {
|
||||||
|
"alpha" : 1,
|
||||||
|
"blue" : 0.19851559400558472,
|
||||||
|
"green" : 0.19851559400558472,
|
||||||
|
"red" : 0.19851559400558472
|
||||||
|
},
|
||||||
|
"fileName" : "1.png",
|
||||||
|
"fontSize" : 107,
|
||||||
|
"key" : "1",
|
||||||
|
"localizations" : {
|
||||||
|
"ar-SA" : "نقل سريع إلى أندرويد والمزيد.",
|
||||||
|
"ca" : "Transferència ràpida a Android i més.",
|
||||||
|
"cs" : "Rychlý přenos na Android i jinam.",
|
||||||
|
"da" : "Hurtig filoverførsel til Android m.m.",
|
||||||
|
"de-DE" : "Schneller Transfer zu Android & mehr.",
|
||||||
|
"el" : "Γρήγορη μεταφορά σε Android και άλλα.",
|
||||||
|
"en-AU" : "Quick Transfer to Android & more.",
|
||||||
|
"en-CA" : "Quick Transfer to Android & more.",
|
||||||
|
"en-GB" : "Quick Transfer to Android & more.",
|
||||||
|
"es-ES" : "Transferencia rápida a Android y más.",
|
||||||
|
"es-MX" : "Transferencia rápida a Android y más.",
|
||||||
|
"fi" : "Nopea tiedostonsiirto Androidille ym.",
|
||||||
|
"fr-CA" : "Transfert rapide vers Android+.",
|
||||||
|
"fr-FR" : "Transfert rapide vers Android+.",
|
||||||
|
"he" : "העברת קבצים מהירה לאנדרואיד ועוד.",
|
||||||
|
"hi" : "एंड्रॉइड आदि पर तेज़ फ़ाइल ट्रांसफर।",
|
||||||
|
"hr" : "Brz prijenos na Android i više.",
|
||||||
|
"hu" : "Gyors fájlátvitel Androidra és másra.",
|
||||||
|
"id" : "Transfer cepat ke Android & lainnya.",
|
||||||
|
"it" : "Trasferisci file su Android e altro.",
|
||||||
|
"ja" : "Android などへのファイルを素早く転送。",
|
||||||
|
"ko" : "Android 등으로 빠른 파일 전송.",
|
||||||
|
"ms" : "Pindahan pantas ke Android+.",
|
||||||
|
"nl-NL" : "Snel bestanden naar Android en meer.",
|
||||||
|
"no" : "Rask overføring til Android og mer.",
|
||||||
|
"pl" : "Szybki transfer na Androida i więcej.",
|
||||||
|
"pt-BR" : "Transfer rápido para Android e mais.",
|
||||||
|
"pt-PT" : "Transfer rápido para Android e mais.",
|
||||||
|
"ro" : "Transfer rapid la Android și altele.",
|
||||||
|
"ru" : "Передача на Android и не только.",
|
||||||
|
"sk" : "Rýchly prenos na Android aj inde.",
|
||||||
|
"sv" : "Snabb överföring till Android m.m.",
|
||||||
|
"th" : "ถ่ายโอนไฟล์เร็วไปยัง Android และอื่นๆ",
|
||||||
|
"tr" : "Android'e hızlı aktarım ve fazlası.",
|
||||||
|
"uk" : "Передача на Android і не тільки.",
|
||||||
|
"vi" : "Chuyển nhanh sang Android và hơn nữa.",
|
||||||
|
"zh-Hans" : "快速文件传输到安卓及更多设备。",
|
||||||
|
"zh-Hant" : "快速檔案傳輸到 Android 及更多裝置。"
|
||||||
|
},
|
||||||
|
"yOffset" : 253
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"baseText" : "Browse & manage received files.",
|
||||||
|
"color" : {
|
||||||
|
"alpha" : 1,
|
||||||
|
"blue" : 0.19851559400558472,
|
||||||
|
"green" : 0.19851559400558472,
|
||||||
|
"red" : 0.19851559400558472
|
||||||
|
},
|
||||||
|
"fileName" : "2.png",
|
||||||
|
"fontSize" : 107,
|
||||||
|
"key" : "2",
|
||||||
|
"localizations" : {
|
||||||
|
"ar-SA" : "تصفح وأدر الملفات المستلمة.",
|
||||||
|
"ca" : "Navega i gestiona els fitxers rebuts.",
|
||||||
|
"cs" : "Přijaté soubory.",
|
||||||
|
"da" : "Se og administrer modtagne filer.",
|
||||||
|
"de-DE" : "Empfangene Dateien ansehen & ordnen.",
|
||||||
|
"el" : "Δες και διαχειρίσου ληφθέντα αρχεία.",
|
||||||
|
"en-AU" : "Browse & manage received files.",
|
||||||
|
"en-CA" : "Browse & manage received files.",
|
||||||
|
"en-GB" : "Browse & manage received files.",
|
||||||
|
"es-ES" : "Archivos recibidos: ver y gestionar.",
|
||||||
|
"es-MX" : "Archivos recibidos: ver y gestionar.",
|
||||||
|
"fi" : "Selaa ja hallitse saatuja tiedostoja.",
|
||||||
|
"fr-CA" : "Parcourez et gérez vos fichiers.",
|
||||||
|
"fr-FR" : "Parcourez et gérez vos fichiers.",
|
||||||
|
"he" : "עיין ונהל קבצים שהתקבלו.",
|
||||||
|
"hi" : "प्राप्त फ़ाइलें देखें व संभालें।",
|
||||||
|
"hr" : "Primljene datoteke.",
|
||||||
|
"hu" : "Fogadott fájlok.",
|
||||||
|
"id" : "Jelajahi & kelola file yang diterima.",
|
||||||
|
"it" : "Sfoglia e gestisci i file ricevuti.",
|
||||||
|
"ja" : "受信ファイルを閲覧&管理",
|
||||||
|
"ko" : "받은 파일을 찾아보고 관리하세요.",
|
||||||
|
"ms" : "Semak & urus fail diterima.",
|
||||||
|
"nl-NL" : "Beheer ontvangen bestanden.",
|
||||||
|
"no" : "Se og administrer mottatte filer.",
|
||||||
|
"pl" : "Odebrane pliki: przegląd i obsługa.",
|
||||||
|
"pt-BR" : "Veja e gerencie arquivos recebidos.",
|
||||||
|
"pt-PT" : "Gere ficheiros recebidos.",
|
||||||
|
"ro" : "Fișiere primite: vezi și gestionează.",
|
||||||
|
"ru" : "Полученные файлы: обзор и управление.",
|
||||||
|
"sk" : "Prehliadaj a spravuj prijaté súbory.",
|
||||||
|
"sv" : "Bläddra och hantera mottagna filer.",
|
||||||
|
"th" : "เรียกดูและจัดการไฟล์ที่ได้รับ",
|
||||||
|
"tr" : "Alınan dosyalara göz atın ve yönetin.",
|
||||||
|
"uk" : "Отримані файли.",
|
||||||
|
"vi" : "Duyệt và quản lý các tệp đã nhận.",
|
||||||
|
"zh-Hans" : "浏览和管理收到的文件。",
|
||||||
|
"zh-Hant" : "瀏覽並管理已接收的檔案。"
|
||||||
|
},
|
||||||
|
"yOffset" : 253
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"baseText" : "Share files right from your apps.",
|
||||||
|
"color" : {
|
||||||
|
"alpha" : 1,
|
||||||
|
"blue" : 0.19851559400558472,
|
||||||
|
"green" : 0.19851559400558472,
|
||||||
|
"red" : 0.19851559400558472
|
||||||
|
},
|
||||||
|
"fileName" : "3.png",
|
||||||
|
"fontSize" : 107,
|
||||||
|
"key" : "3",
|
||||||
|
"localizations" : {
|
||||||
|
"ar-SA" : "شارك الملفات مباشرة من تطبيقاتك.",
|
||||||
|
"ca" : "Comparteix fitxers des de les apps.",
|
||||||
|
"cs" : "Sdílejte soubory přímo z aplikací.",
|
||||||
|
"da" : "Del filer direkte fra dine apps.",
|
||||||
|
"de-DE" : "Teile Dateien direkt aus deinen Apps.",
|
||||||
|
"el" : "Μοιράσου αρχεία από τις εφαρμογές.",
|
||||||
|
"en-AU" : "Share files right from your apps.",
|
||||||
|
"en-CA" : "Share files right from your apps.",
|
||||||
|
"en-GB" : "Share files right from your apps.",
|
||||||
|
"es-ES" : "Comparte archivos desde tus apps.",
|
||||||
|
"es-MX" : "Comparte archivos desde tus apps.",
|
||||||
|
"fi" : "Jaa tiedostoja sovelluksistasi.",
|
||||||
|
"fr-CA" : "Partagez des fichiers via vos apps.",
|
||||||
|
"fr-FR" : "Partagez des fichiers via vos apps.",
|
||||||
|
"he" : "שתף קבצים ישירות מהאפליקציות שלך.",
|
||||||
|
"hi" : "अपने ऐप्स से सीधे फ़ाइलें साझा करें।",
|
||||||
|
"hr" : "Dijeli datoteke iz svojih aplikacija.",
|
||||||
|
"hu" : "Ossz meg fájlokat az appjaidból.",
|
||||||
|
"id" : "Bagikan file dari aplikasimu.",
|
||||||
|
"it" : "Condividi file dalle tue app.",
|
||||||
|
"ja" : "アプリから直接ファイルを共有できます。",
|
||||||
|
"ko" : "앱에서 바로 파일을 공유하세요.",
|
||||||
|
"ms" : "Kongsi fail terus dari aplikasi anda.",
|
||||||
|
"nl-NL" : "Deel bestanden direct vanuit je apps.",
|
||||||
|
"no" : "Del filer direkte fra appene dine.",
|
||||||
|
"pl" : "Udostępniaj pliki z aplikacji.",
|
||||||
|
"pt-BR" : "Compartilhe arquivos direto dos apps.",
|
||||||
|
"pt-PT" : "Partilha ficheiros das tuas apps.",
|
||||||
|
"ro" : "Partajează fișiere din aplicații.",
|
||||||
|
"ru" : "Делитесь файлами из приложений.",
|
||||||
|
"sk" : "Zdieľaj súbory zo svojich aplikácií.",
|
||||||
|
"sv" : "Dela filer direkt från dina appar.",
|
||||||
|
"th" : "แชร์ไฟล์ได้โดยตรงจากแอปของคุณ",
|
||||||
|
"tr" : "Uygulamalardan paylaş.",
|
||||||
|
"uk" : "Діліться файлами прямо з додатків.",
|
||||||
|
"vi" : "Chia sẻ tệp ngay từ ứng dụng của bạn.",
|
||||||
|
"zh-Hans" : "直接从你的应用中分享文件。",
|
||||||
|
"zh-Hant" : "直接從你的應用程式分享檔案。"
|
||||||
|
},
|
||||||
|
"yOffset" : 253
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"baseText" : "Ultra-fast transfer over Wi-Fi.",
|
||||||
|
"color" : {
|
||||||
|
"alpha" : 1,
|
||||||
|
"blue" : 0.19851559400558472,
|
||||||
|
"green" : 0.19851559400558472,
|
||||||
|
"red" : 0.19851559400558472
|
||||||
|
},
|
||||||
|
"fileName" : "4.png",
|
||||||
|
"fontSize" : 107,
|
||||||
|
"key" : "4",
|
||||||
|
"localizations" : {
|
||||||
|
"ar-SA" : "نقل فائق السرعة عبر الواي فاي.",
|
||||||
|
"ca" : "Transferència Wi-Fi ultraràpida.",
|
||||||
|
"cs" : "Ultra-rychlý přenos přes Wi-Fi.",
|
||||||
|
"da" : "Ultrahurtig overførsel via Wi-Fi.",
|
||||||
|
"de-DE" : "Ultraschneller Transfer über WLAN.",
|
||||||
|
"el" : "Ταχύτατη μεταφορά Wi-Fi.",
|
||||||
|
"en-AU" : "Ultra-fast transfer over Wi-Fi.",
|
||||||
|
"en-CA" : "Ultra-fast transfer over Wi-Fi.",
|
||||||
|
"en-GB" : "Ultra-fast transfer over Wi-Fi.",
|
||||||
|
"es-ES" : "Transfiere rápido por Wi-Fi.",
|
||||||
|
"es-MX" : "Transfiere rápido por Wi-Fi.",
|
||||||
|
"fi" : "Huippunopea siirto Wi-Fillä.",
|
||||||
|
"fr-CA" : "Transfert Wi-Fi ultra-rapide.",
|
||||||
|
"fr-FR" : "Transfert Wi-Fi ultra-rapide.",
|
||||||
|
"he" : "העברה מהירה במיוחד דרך Wi-Fi.",
|
||||||
|
"hi" : "Wi-Fi पर अल्ट्रा-फास्ट ट्रांसफर।",
|
||||||
|
"hr" : "Ultra-brz prijenos putem Wi-Fi-ja.",
|
||||||
|
"hu" : "Ultragyors átvitel Wi-Fin.",
|
||||||
|
"id" : "Transfer ultra-cepat melalui Wi-Fi.",
|
||||||
|
"it" : "Transfer rapido via Wi-Fi.",
|
||||||
|
"ja" : "Wi-Fi経由で超高速転送。",
|
||||||
|
"ko" : "Wi-Fi를 통한 초고속 전송.",
|
||||||
|
"ms" : "Pindahan ultra-pantas melalui Wi-Fi.",
|
||||||
|
"nl-NL" : "Ultrasnelle overdracht via Wi-Fi.",
|
||||||
|
"no" : "Ultrarask overføring over Wi-Fi.",
|
||||||
|
"pl" : "Błyskawiczny transfer przez Wi-Fi.",
|
||||||
|
"pt-BR" : "Transfira muito rápido via Wi-Fi.",
|
||||||
|
"pt-PT" : "Transfere muito rápido por Wi-Fi.",
|
||||||
|
"ro" : "Transfer ultra-rapid prin Wi-Fi.",
|
||||||
|
"ru" : "Быстрая передача Wi-Fi.",
|
||||||
|
"sk" : "Ultra-rýchly prenos cez Wi-Fi.",
|
||||||
|
"sv" : "Ultrasnabb överföring via Wi-Fi.",
|
||||||
|
"th" : "ถ่ายโอนด้วยความเร็วสูงผ่าน Wi-Fi",
|
||||||
|
"tr" : "Wi-Fi ile ultra hızlı transfer.",
|
||||||
|
"uk" : "Надшвидка передача через Wi-Fi.",
|
||||||
|
"vi" : "Chuyển siêu nhanh qua Wi-Fi.",
|
||||||
|
"zh-Hans" : "超快的 Wi-Fi 传输。",
|
||||||
|
"zh-Hant" : "超快速 Wi-Fi 傳輸。"
|
||||||
|
},
|
||||||
|
"yOffset" : 253
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sourceLocale" : "en-US",
|
||||||
|
"syncSettings" : true,
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@@ -12,6 +12,7 @@ import Network
|
|||||||
import System
|
import System
|
||||||
import BigInt
|
import BigInt
|
||||||
import SwiftECC
|
import SwiftECC
|
||||||
|
import ASN1
|
||||||
import LUI
|
import LUI
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@@ -29,8 +30,14 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
private var textPayloadID: Int64 = 0
|
private var textPayloadID: Int64 = 0
|
||||||
private var bytesToBeTransferred: Int64 = 0
|
private var bytesToBeTransferred: Int64 = 0
|
||||||
private var isAuthenticated = false
|
private var isAuthenticated = false
|
||||||
|
private var receiverAuthenticationAccepted = false
|
||||||
private var peerCertificate: Sharing_Nearby_PublicCertificate? = nil
|
private var peerCertificate: Sharing_Nearby_PublicCertificate? = nil
|
||||||
|
private var mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata?
|
||||||
|
private var pairingMetadata: Sharing_Nearby_PairingMetadata?
|
||||||
|
private var isPairingSetupRequest = false
|
||||||
var isPlainTextTransfer = false
|
var isPlainTextTransfer = false
|
||||||
|
var isClipboardSyncTransfer = false
|
||||||
|
var isControlTransfer = false
|
||||||
|
|
||||||
var wasUserRejected = false
|
var wasUserRejected = false
|
||||||
var delegate: InboundNearbyConnectionDelegate?
|
var delegate: InboundNearbyConnectionDelegate?
|
||||||
@@ -46,6 +53,16 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
|
|
||||||
override func disconnect() {
|
override func disconnect() {
|
||||||
|
let recoveredAllPendingFiles = recoverCompletedFilesOnDisconnect()
|
||||||
|
if recoveredAllPendingFiles {
|
||||||
|
if case let .protocolError(message) = self.lastError as? NearbyError,
|
||||||
|
message == "Error.ClosedByPeer".localized() {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Recovering completed transfer after connection closed by peer.")
|
||||||
|
self.lastError = nil
|
||||||
|
NearbyConnectionManager.shared.updatedTransferProgress(connection: self, progress: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
super.disconnect()
|
super.disconnect()
|
||||||
currentState = .disconnected
|
currentState = .disconnected
|
||||||
deletePartiallyReceivedFiles()
|
deletePartiallyReceivedFiles()
|
||||||
@@ -61,6 +78,19 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
if currentState != .receivingFiles {
|
if currentState != .receivingFiles {
|
||||||
log("[InboundNearbyConnection \(self.id)] Received frame in state \(currentState)...")
|
log("[InboundNearbyConnection \(self.id)] Received frame in state \(currentState)...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !encryptionDone,
|
||||||
|
currentState != .initial,
|
||||||
|
let frame = try? Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData),
|
||||||
|
frame.hasV1,
|
||||||
|
frame.v1.hasType,
|
||||||
|
case .keepAlive = frame.v1.type {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Received keep-alive during handshake in state \(currentState)")
|
||||||
|
if !frame.v1.hasKeepAlive || !frame.v1.keepAlive.ack {
|
||||||
|
sendKeepAlive(ack: true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
switch currentState {
|
switch currentState {
|
||||||
@@ -111,11 +141,64 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
}
|
}
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case .sentConnectionResponse:
|
case .sentConnectionResponse:
|
||||||
try processPairedKeyEncryptionFrame(frame)
|
if frame.hasV1, frame.v1.hasPairedKeyEncryption {
|
||||||
|
try processPairedKeyEncryptionFrame(frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasIntroduction {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Received introduction before paired-key exchange finished; continuing unauthenticated flow.")
|
||||||
|
try sendPairedKeyResult(status: .unable)
|
||||||
|
try processIntroductionFrame(frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .pairedKeyResult || frame.v1.type == .progressUpdate {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for pairedKeyEncryption.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.v1.type == .response, case .accept = frame.v1.connectionResponse.status {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring accept response while waiting for pairedKeyEncryption.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyEncryption")
|
||||||
case .sentPairedKeyResult:
|
case .sentPairedKeyResult:
|
||||||
try processPairedKeyResultFrame(frame)
|
if frame.hasV1, frame.v1.hasPairedKeyResult {
|
||||||
|
try processPairedKeyResultFrame(frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasIntroduction {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Received introduction without pairedKeyResult; continuing for legacy compatibility.")
|
||||||
|
try processIntroductionFrame(frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .pairedKeyEncryption || frame.v1.type == .progressUpdate {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for pairedKeyResult.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.v1.type == .response, case .accept = frame.v1.connectionResponse.status {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring accept response while waiting for pairedKeyResult.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult")
|
||||||
case .receivedPairedKeyResult:
|
case .receivedPairedKeyResult:
|
||||||
try processIntroductionFrame(frame)
|
if frame.hasV1, frame.v1.hasIntroduction {
|
||||||
|
try processIntroductionFrame(frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .pairedKeyResult || frame.v1.type == .pairedKeyEncryption || frame.v1.type == .progressUpdate {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for introduction.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.v1.type == .response, case .accept = frame.v1.connectionResponse.status {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring accept response while waiting for introduction.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.introduction")
|
||||||
default:
|
default:
|
||||||
if frame.hasV1, frame.v1.hasType, frame.v1.type == .progressUpdate {
|
if frame.hasV1, frame.v1.hasType, frame.v1.type == .progressUpdate {
|
||||||
// ignore progress updates
|
// ignore progress updates
|
||||||
@@ -140,19 +223,23 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
let id = frame.payloadHeader.id
|
let id = frame.payloadHeader.id
|
||||||
|
|
||||||
guard let fileInfo = filesToBeReceived[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") }
|
guard var fileInfo = filesToBeReceived[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") }
|
||||||
|
|
||||||
let currentOffset = fileInfo.bytesTransferred
|
let currentOffset = fileInfo.bytesTransferred
|
||||||
|
|
||||||
guard frame.payloadChunk.offset == currentOffset else { throw NearbyError.protocolError("Invalid offset into file \(frame.payloadChunk.offset), expected \(currentOffset)") }
|
guard frame.payloadChunk.offset == currentOffset else { throw NearbyError.protocolError("Invalid offset into file \(frame.payloadChunk.offset), expected \(currentOffset)") }
|
||||||
|
|
||||||
guard currentOffset + Int64(frame.payloadChunk.body.count) <= fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") }
|
guard currentOffset + Int64(frame.payloadChunk.body.count) <= fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") }
|
||||||
|
|
||||||
if !frame.payloadChunk.body.isEmpty {
|
let hasBody = !frame.payloadChunk.body.isEmpty
|
||||||
|
let isLastChunk = (frame.payloadChunk.flags & 1) == 1
|
||||||
|
|
||||||
|
if hasBody {
|
||||||
do {
|
do {
|
||||||
try fileInfo.fileHandle?.write(contentsOf: frame.payloadChunk.body)
|
try fileInfo.fileHandle?.write(contentsOf: frame.payloadChunk.body)
|
||||||
filesToBeReceived[id]!.bytesTransferred += Int64(frame.payloadChunk.body.count)
|
fileInfo.bytesTransferred += Int64(frame.payloadChunk.body.count)
|
||||||
fileInfo.progress?.completedUnitCount = filesToBeReceived[id]!.bytesTransferred
|
fileInfo.progress?.completedUnitCount = fileInfo.bytesTransferred
|
||||||
|
filesToBeReceived[id] = fileInfo
|
||||||
|
|
||||||
self.bytesTransferred += Int64(frame.payloadChunk.body.count)
|
self.bytesTransferred += Int64(frame.payloadChunk.body.count)
|
||||||
NearbyConnectionManager.shared.updatedTransferProgress(connection: self, progress: Double(self.bytesTransferred) / Double(self.bytesToBeTransferred))
|
NearbyConnectionManager.shared.updatedTransferProgress(connection: self, progress: Double(self.bytesTransferred) / Double(self.bytesToBeTransferred))
|
||||||
@@ -162,15 +249,19 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
throw NearbyError.protocolError(error.localizedDescription)
|
throw NearbyError.protocolError(error.localizedDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (frame.payloadChunk.flags & 1) == 1 {
|
|
||||||
|
if isLastChunk {
|
||||||
|
guard fileInfo.bytesTransferred == fileInfo.meta.size else {
|
||||||
|
throw NearbyError.protocolError("Received EOF before file was fully transferred (\(fileInfo.bytesTransferred)/\(fileInfo.meta.size) bytes)")
|
||||||
|
}
|
||||||
try fileInfo.fileHandle?.close()
|
try fileInfo.fileHandle?.close()
|
||||||
filesToBeReceived[id]!.fileHandle = nil
|
fileInfo.fileHandle = nil
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
fileInfo.progress?.unpublish()
|
fileInfo.progress?.unpublish()
|
||||||
#endif
|
#endif
|
||||||
downloadedFiles.append(fileInfo.destinationURL)
|
downloadedFiles.append(fileInfo.destinationURL)
|
||||||
EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL)
|
EXIFUtils.applyTimestamps(at: fileInfo.destinationURL)
|
||||||
|
filesToBeReceived[id] = fileInfo
|
||||||
filesToBeReceived.removeValue(forKey: id)
|
filesToBeReceived.removeValue(forKey: id)
|
||||||
|
|
||||||
if filesToBeReceived.isEmpty {
|
if filesToBeReceived.isEmpty {
|
||||||
@@ -179,7 +270,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
try sendDisconnectionAndDisconnect()
|
try sendDisconnectionAndDisconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else if !hasBody {
|
||||||
log("[InboundNearbyConnection \(self.id)] Received file chunk with no body and no flags, ignoring it.")
|
log("[InboundNearbyConnection \(self.id)] Received file chunk with no body and no flags, ignoring it.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,11 +281,22 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
if let urlStr = String(data: payload, encoding: .utf8) {
|
if let urlStr = String(data: payload, encoding: .utf8) {
|
||||||
|
|
||||||
if isPlainTextTransfer {
|
if isPlainTextTransfer {
|
||||||
|
#if os(macOS) && !EXTENSION
|
||||||
|
ClipboardSyncCoordinator.shared.suppressNextClipboardSync(for: urlStr)
|
||||||
|
#endif
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
// macOS clipboard
|
// macOS clipboard
|
||||||
let pasteboard = NSPasteboard.general
|
let pasteboard = NSPasteboard.general
|
||||||
pasteboard.clearContents()
|
pasteboard.clearContents()
|
||||||
pasteboard.setString(urlStr, forType: .string)
|
pasteboard.setString(urlStr, forType: .string)
|
||||||
|
#if !EXTENSION
|
||||||
|
if isClipboardSyncTransfer {
|
||||||
|
ClipboardSyncNotificationPresenter.shared.present(
|
||||||
|
clipboardText: urlStr,
|
||||||
|
senderDeviceName: remoteDeviceInfo?.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
// iOS clipboard
|
// iOS clipboard
|
||||||
UIPasteboard.general.string = urlStr
|
UIPasteboard.general.string = urlStr
|
||||||
@@ -223,7 +325,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
#endif
|
#endif
|
||||||
filesToBeReceived.removeValue(forKey: id)
|
filesToBeReceived.removeValue(forKey: id)
|
||||||
downloadedFiles.append(fileInfo.destinationURL)
|
downloadedFiles.append(fileInfo.destinationURL)
|
||||||
EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL)
|
EXIFUtils.applyTimestamps(at: fileInfo.destinationURL)
|
||||||
|
|
||||||
if filesToBeReceived.isEmpty {
|
if filesToBeReceived.isEmpty {
|
||||||
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
|
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
|
||||||
@@ -243,8 +345,11 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
guard case .connectionRequest = frame.v1.type else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
|
guard case .connectionRequest = frame.v1.type else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
|
||||||
|
|
||||||
let endpointInfo = EndpointInfo(data: frame.v1.connectionRequest.endpointInfo)
|
let endpointInfo = EndpointInfo(data: frame.v1.connectionRequest.endpointInfo)
|
||||||
|
let fallbackDeviceName = "AndroidDevice".localized()
|
||||||
|
let resolvedDeviceName = endpointInfo?.name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let deviceName = (resolvedDeviceName?.isEmpty == false) ? resolvedDeviceName : fallbackDeviceName
|
||||||
|
|
||||||
remoteDeviceInfo = RemoteDeviceInfo(name: endpointInfo?.name, type: endpointInfo?.deviceType)
|
remoteDeviceInfo = RemoteDeviceInfo(name: deviceName, type: endpointInfo?.deviceType)
|
||||||
currentState = .receivedConnectionRequest
|
currentState = .receivedConnectionRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,10 +469,29 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
pairedEncryption.v1 = Sharing_Nearby_V1Frame()
|
pairedEncryption.v1 = Sharing_Nearby_V1Frame()
|
||||||
pairedEncryption.v1.type = .pairedKeyEncryption
|
pairedEncryption.v1.type = .pairedKeyEncryption
|
||||||
pairedEncryption.v1.pairedKeyEncryption = Sharing_Nearby_PairedKeyEncryptionFrame()
|
pairedEncryption.v1.pairedKeyEncryption = Sharing_Nearby_PairedKeyEncryptionFrame()
|
||||||
|
|
||||||
// For inbound connections, we do not use provide secretIDHash and signedData
|
if let signingPrivateKey = IdentityManager.shared.getPrivateKey(),
|
||||||
pairedEncryption.v1.pairedKeyEncryption.secretIDHash = Data.randomData(length: 6)
|
let publicKey = IdentityManager.shared.getPublicKey()?.toGenericPublicKey(),
|
||||||
pairedEncryption.v1.pairedKeyEncryption.signedData = Data.randomData(length: 72)
|
let publicKeyData = IdentityManager.shared.getPublicKey()?.toGenericPublicKeyData(),
|
||||||
|
let publicKeyID = publicKey.id(),
|
||||||
|
let authKeyData = self.authKey?.data() {
|
||||||
|
|
||||||
|
var cert = Sharing_Nearby_PublicCertificate()
|
||||||
|
cert.secretID = publicKeyID
|
||||||
|
cert.publicKey = publicKeyData
|
||||||
|
|
||||||
|
let signatureTuple = signingPrivateKey.sign(msg: authKeyData)
|
||||||
|
|
||||||
|
pairedEncryption.v1.certificateInfo.publicCertificate.append(cert)
|
||||||
|
pairedEncryption.v1.pairedKeyEncryption.secretIDHash = cert.secretID
|
||||||
|
pairedEncryption.v1.pairedKeyEncryption.signedData = Data(signatureTuple.asn1.encode())
|
||||||
|
|
||||||
|
pairedEncryption.v1.pairedKeyResult.status = .success
|
||||||
|
} else {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] No private key available for receiver authentication.")
|
||||||
|
pairedEncryption.v1.pairedKeyEncryption.secretIDHash = Data.randomData(length: 6)
|
||||||
|
pairedEncryption.v1.pairedKeyEncryption.signedData = Data.randomData(length: 72)
|
||||||
|
}
|
||||||
|
|
||||||
try sendTransferSetupFrame(pairedEncryption)
|
try sendTransferSetupFrame(pairedEncryption)
|
||||||
currentState = .sentConnectionResponse
|
currentState = .sentConnectionResponse
|
||||||
@@ -414,6 +538,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
if peerPublicKey.verify(signature: signature, msg: authKeyData) {
|
if peerPublicKey.verify(signature: signature, msg: authKeyData) {
|
||||||
log("[InboundNearbyConnection \(self.id)] Paired key authentication successful.")
|
log("[InboundNearbyConnection \(self.id)] Paired key authentication successful.")
|
||||||
|
self.peerCertificate = trustedCert
|
||||||
try sendPairedKeyResult(status: .success)
|
try sendPairedKeyResult(status: .success)
|
||||||
isAuthenticated = true
|
isAuthenticated = true
|
||||||
} else {
|
} else {
|
||||||
@@ -455,6 +580,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
private func processPairedKeyResultFrame(_ frame: Sharing_Nearby_Frame) throws {
|
private func processPairedKeyResultFrame(_ frame: Sharing_Nearby_Frame) throws {
|
||||||
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") }
|
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") }
|
||||||
|
receiverAuthenticationAccepted = frame.v1.pairedKeyResult.status == .success
|
||||||
currentState = .receivedPairedKeyResult
|
currentState = .receivedPairedKeyResult
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,16 +630,162 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
}
|
}
|
||||||
let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
||||||
checkIfCanProceed(metadata: metadata)
|
checkIfCanProceed(metadata: metadata)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
else if let textMetadata = frame.v1.introduction.textMetadata.first {
|
|
||||||
|
#if os(macOS)
|
||||||
|
if frame.v1.introduction.hasMirroredNotificationMetadata || frame.v1.introduction.hasPairingMetadata {
|
||||||
|
isControlTransfer = true
|
||||||
|
mirroredNotificationMetadata = frame.v1.introduction.hasMirroredNotificationMetadata
|
||||||
|
? frame.v1.introduction.mirroredNotificationMetadata
|
||||||
|
: nil
|
||||||
|
pairingMetadata = frame.v1.introduction.hasPairingMetadata
|
||||||
|
? frame.v1.introduction.pairingMetadata
|
||||||
|
: nil
|
||||||
|
isPairingSetupRequest = pairingMetadata?.setupRequest == true
|
||||||
|
let hasPairingConfirmation = pairingMetadata?.hasSetupPinHash == true
|
||||||
|
let pairingUseCase = pairingMetadata.flatMap { PairingUseCase(protoValue: $0.useCase) }
|
||||||
|
|
||||||
|
guard let device = self.remoteDeviceInfo else {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if pairingMetadata != nil && pairingUseCase == nil {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if pairingMetadata != nil,
|
||||||
|
mirroredNotificationMetadata == nil,
|
||||||
|
!isPairingSetupRequest,
|
||||||
|
!hasPairingConfirmation {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAuthenticated && !isPairingSetupRequest && !hasPairingConfirmation {
|
||||||
|
self.lastError = NearbyError.notificationSyncNotTrusted
|
||||||
|
self.rejectTransfer(with: .reject, markAsUserRejected: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasPairingConfirmation {
|
||||||
|
guard let peerCertificate = peerCertificate,
|
||||||
|
let pairingMetadata,
|
||||||
|
let pairingUseCase else {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Existing trust only proves this is the same device key we trusted before.
|
||||||
|
// It does not by itself grant a new feature, such as clipboard sync.
|
||||||
|
let trustedData = TrustStore.shared.findTrustedKey(for: peerCertificate.secretID)
|
||||||
|
if let trustedData {
|
||||||
|
guard let currentData = try? peerCertificate.serializedData(),
|
||||||
|
currentData == trustedData else {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The QR token proves the user accepted this specific pairing use case.
|
||||||
|
// Require it even for already-trusted devices before adding a new grant.
|
||||||
|
let matches = TrustStore.shared.confirmPendingPairingTrust(
|
||||||
|
secretIdHex: peerCertificate.secretID.hex,
|
||||||
|
useCase: pairingUseCase,
|
||||||
|
pinHash: pairingMetadata.setupPinHash
|
||||||
|
)
|
||||||
|
|
||||||
|
guard matches else {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve the existing trusted certificate when present; otherwise store
|
||||||
|
// the newly paired device and attach the confirmed use-case grant.
|
||||||
|
if trustedData != nil {
|
||||||
|
TrustStore.shared.grantUseCase(pairingUseCase, for: peerCertificate.secretID.hex)
|
||||||
|
} else {
|
||||||
|
TrustStore.shared.addTrusted(
|
||||||
|
certificate: peerCertificate,
|
||||||
|
device: device,
|
||||||
|
grantedUseCases: [pairingUseCase]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.pairingSetupConfirmed(connection: self)
|
||||||
|
}
|
||||||
|
submitUserConsent(accepted: true, trustDevice: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAuthenticated || isPairingSetupRequest {
|
||||||
|
guard let transferType = pairingTransferType(for: pairingUseCase) else {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let metadata = TransferMetadata(
|
||||||
|
files: [],
|
||||||
|
id: id,
|
||||||
|
pinCode: pinCode,
|
||||||
|
transferType: transferType,
|
||||||
|
allowsToBeAddedAsTrustedDevice: true
|
||||||
|
)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.obtainUserConsent(transfer: metadata, device: device, connection: self)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitUserConsent(accepted: true, trustDevice: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if let textMetadata = frame.v1.introduction.textMetadata.first {
|
||||||
let isURL = textMetadata.type == .url
|
let isURL = textMetadata.type == .url
|
||||||
|
|
||||||
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isURL ? .url : .text, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
|
||||||
textPayloadID = textMetadata.payloadID
|
textPayloadID = textMetadata.payloadID
|
||||||
|
let isRemoteCopy = frame.v1.introduction.hasUseCase
|
||||||
|
&& frame.v1.introduction.useCase == .remoteCopy
|
||||||
|
if isRemoteCopy && !isURL {
|
||||||
|
let peerFingerprint = peerCertificate?.secretID.hex
|
||||||
|
let isClipboardSyncAllowed = isAuthenticated
|
||||||
|
&& receiverAuthenticationAccepted
|
||||||
|
&& TrustStore.shared.hasUseCaseGrant(.clipboardSync, for: peerFingerprint)
|
||||||
|
guard isClipboardSyncAllowed else {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Rejecting clipboard sync transfer because the sender is not trusted for clipboard sync.")
|
||||||
|
self.rejectTransfer(with: .reject, markAsUserRejected: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isClipboardSyncTransfer = true
|
||||||
|
} else {
|
||||||
|
isClipboardSyncTransfer = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = TransferMetadata(
|
||||||
|
files: [],
|
||||||
|
id: id,
|
||||||
|
pinCode: pinCode,
|
||||||
|
textDescription: textMetadata.textTitle,
|
||||||
|
transferType: isURL ? .url : .text,
|
||||||
|
allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil,
|
||||||
|
isClipboardSync: isClipboardSyncTransfer
|
||||||
|
)
|
||||||
|
|
||||||
if !isURL {
|
if !isURL {
|
||||||
isPlainTextTransfer = true
|
isPlainTextTransfer = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkIfCanProceed(metadata: metadata)
|
||||||
|
}
|
||||||
|
else if let wifiMetadata = frame.v1.introduction.wifiCredentialsMetadata.first {
|
||||||
|
|
||||||
|
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: wifiMetadata.ssid, transferType: .wifiPassword, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
||||||
|
textPayloadID = wifiMetadata.payloadID
|
||||||
|
|
||||||
|
isPlainTextTransfer = true
|
||||||
|
|
||||||
checkIfCanProceed(metadata: metadata)
|
checkIfCanProceed(metadata: metadata)
|
||||||
}
|
}
|
||||||
@@ -527,7 +799,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
func checkIfCanProceed(metadata: TransferMetadata) {
|
func checkIfCanProceed(metadata: TransferMetadata) {
|
||||||
|
|
||||||
let acceptAutomatically = isAuthenticated || Settings.shared.automaticallyAcceptFiles
|
let acceptAutomatically = isAuthenticated || Settings.sharedInstance.automaticallyAcceptFiles
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if acceptAutomatically {
|
if acceptAutomatically {
|
||||||
@@ -542,7 +814,28 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func submitUserConsent(accepted: Bool, trustDevice: Bool) {
|
func submitUserConsent(accepted: Bool, trustDevice: Bool, pairingToken: String? = nil) {
|
||||||
|
if isPairingSetupRequest,
|
||||||
|
let peerCertificate = peerCertificate,
|
||||||
|
let pairingMetadata,
|
||||||
|
let pairingUseCase = PairingUseCase(protoValue: pairingMetadata.useCase) {
|
||||||
|
let secretIdHex = peerCertificate.secretID.hex
|
||||||
|
if accepted {
|
||||||
|
if let token = pairingToken {
|
||||||
|
TrustStore.shared.registerPendingPairingTrust(
|
||||||
|
secretIdHex: secretIdHex,
|
||||||
|
pinCode: token,
|
||||||
|
useCase: pairingUseCase
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Missing pairing token; pending trust not registered.")
|
||||||
|
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if trustDevice, let peerCertificate = peerCertificate, let remoteDeviceInfo = remoteDeviceInfo {
|
if trustDevice, let peerCertificate = peerCertificate, let remoteDeviceInfo = remoteDeviceInfo {
|
||||||
TrustStore.shared.addTrusted(certificate: peerCertificate, device: remoteDeviceInfo)
|
TrustStore.shared.addTrusted(certificate: peerCertificate, device: remoteDeviceInfo)
|
||||||
}
|
}
|
||||||
@@ -563,7 +856,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if isFileTransferRestricted() {
|
if !isControlTransfer && isFileTransferRestricted() {
|
||||||
log("[InboundNearbyConnection \(self.id)] File transfer restrictions detected.")
|
log("[InboundNearbyConnection \(self.id)] File transfer restrictions detected.")
|
||||||
delegate?.showPlusScreen()
|
delegate?.showPlusScreen()
|
||||||
rejectTransfer()
|
rejectTransfer()
|
||||||
@@ -607,17 +900,36 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
currentState = .receivingFiles
|
currentState = .receivingFiles
|
||||||
isTransferring = true
|
isTransferring = true
|
||||||
try sendTransferSetupFrame(frame)
|
try sendTransferSetupFrame(frame)
|
||||||
|
|
||||||
|
if let mirroredNotificationMetadata,
|
||||||
|
!isPairingSetupRequest {
|
||||||
|
MirroredNotificationPresenter.shared.present(metadata: mirroredNotificationMetadata, senderDeviceName: remoteDeviceInfo?.name)
|
||||||
|
}
|
||||||
|
if isControlTransfer {
|
||||||
|
try sendDisconnectionAndDisconnect()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
lastError = error
|
lastError = error
|
||||||
protocolError()
|
protocolError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func pairingTransferType(for useCase: PairingUseCase?) -> TransferMetadata.TransferType? {
|
||||||
|
switch useCase {
|
||||||
|
case .notificationSync:
|
||||||
|
return .notificationSync
|
||||||
|
case .clipboardSync:
|
||||||
|
return .clipboardSync
|
||||||
|
case .none:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func rejectTransfer(with reason: Sharing_Nearby_ConnectionResponseFrame.Status = .reject) {
|
private func rejectTransfer(with reason: Sharing_Nearby_ConnectionResponseFrame.Status = .reject, markAsUserRejected: Bool = true) {
|
||||||
|
|
||||||
// rejected by user
|
// rejected by user
|
||||||
if reason == .reject {
|
if reason == .reject && markAsUserRejected {
|
||||||
self.wasUserRejected = true
|
self.wasUserRejected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,4 +962,41 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
try? FileManager.default.removeItem(at: file.destinationURL)
|
try? FileManager.default.removeItem(at: file.destinationURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func recoverCompletedFilesOnDisconnect() -> Bool {
|
||||||
|
guard !filesToBeReceived.isEmpty else { return false }
|
||||||
|
|
||||||
|
var recoveredCount = 0
|
||||||
|
let pendingFileIDs = Array(filesToBeReceived.keys)
|
||||||
|
|
||||||
|
for fileID in pendingFileIDs {
|
||||||
|
guard let file = filesToBeReceived[fileID] else { continue }
|
||||||
|
guard file.created, file.bytesTransferred == file.meta.size else { continue }
|
||||||
|
|
||||||
|
do {
|
||||||
|
try file.fileHandle?.close()
|
||||||
|
} catch {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Failed to close recovered file handle for \(file.destinationURL.lastPathComponent): \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
file.progress?.unpublish()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if !downloadedFiles.contains(file.destinationURL) {
|
||||||
|
downloadedFiles.append(file.destinationURL)
|
||||||
|
}
|
||||||
|
EXIFUtils.applyTimestamps(at: file.destinationURL)
|
||||||
|
|
||||||
|
filesToBeReceived.removeValue(forKey: fileID)
|
||||||
|
recoveredCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if recoveredCount > 0 {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Recovered \(recoveredCount) completed file(s) during disconnect.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return recoveredCount > 0 && filesToBeReceived.isEmpty
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//
|
||||||
|
// MirroredNotificationSupport.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
import UserNotifications
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
final class MirroredNotificationPresenter {
|
||||||
|
|
||||||
|
static let shared = MirroredNotificationPresenter()
|
||||||
|
|
||||||
|
private let center = UNUserNotificationCenter.current()
|
||||||
|
private let queue = DispatchQueue(label: "MirroredNotificationPresenter")
|
||||||
|
private var pendingNotifications: [(Sharing_Nearby_MirroredNotificationMetadata, String?)] = []
|
||||||
|
private var authorizationRequestInFlight = false
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func present(metadata: Sharing_Nearby_MirroredNotificationMetadata, senderDeviceName: String?) {
|
||||||
|
queue.async {
|
||||||
|
self.postNotificationIfAuthorized(metadata: metadata, senderDeviceName: senderDeviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func postNotificationIfAuthorized(metadata: Sharing_Nearby_MirroredNotificationMetadata, senderDeviceName: String?) {
|
||||||
|
center.getNotificationSettings { settings in
|
||||||
|
self.queue.async {
|
||||||
|
self.handleAuthorizationStatus(
|
||||||
|
settings.authorizationStatus,
|
||||||
|
metadata: metadata,
|
||||||
|
senderDeviceName: senderDeviceName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleAuthorizationStatus(
|
||||||
|
_ status: UNAuthorizationStatus,
|
||||||
|
metadata: Sharing_Nearby_MirroredNotificationMetadata,
|
||||||
|
senderDeviceName: String?
|
||||||
|
) {
|
||||||
|
switch status {
|
||||||
|
case .authorized, .provisional, .ephemeral:
|
||||||
|
post(metadata: metadata, senderDeviceName: senderDeviceName)
|
||||||
|
case .notDetermined:
|
||||||
|
pendingNotifications.append((metadata, senderDeviceName))
|
||||||
|
requestAuthorizationIfNeeded()
|
||||||
|
case .denied:
|
||||||
|
log("[MirroredNotificationPresenter] Notifications are denied in system settings; cannot mirror.")
|
||||||
|
@unknown default:
|
||||||
|
log("[MirroredNotificationPresenter] Unknown notification authorization status; skipping mirrored notification.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestAuthorizationIfNeeded() {
|
||||||
|
if authorizationRequestInFlight {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizationRequestInFlight = true
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
||||||
|
self.queue.async {
|
||||||
|
self.authorizationRequestInFlight = false
|
||||||
|
|
||||||
|
if let error = error {
|
||||||
|
log("[MirroredNotificationPresenter] Authorization request failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard granted else {
|
||||||
|
log("[MirroredNotificationPresenter] Authorization request not granted; dropping queued mirrored notifications.")
|
||||||
|
self.pendingNotifications.removeAll()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let queued = self.pendingNotifications
|
||||||
|
self.pendingNotifications.removeAll()
|
||||||
|
for (queuedMetadata, queuedSender) in queued {
|
||||||
|
self.post(metadata: queuedMetadata, senderDeviceName: queuedSender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func post(metadata: Sharing_Nearby_MirroredNotificationMetadata, senderDeviceName: String?) {
|
||||||
|
let senderName = senderDeviceName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let hasSenderName = senderName?.isEmpty == false
|
||||||
|
let displaySender = hasSenderName ? senderName! : "NotificationSyncFallbackSenderAndroid".localized()
|
||||||
|
let identifierSender = hasSenderName ? senderName! : "android"
|
||||||
|
let cleanAppName = metadata.appName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let cleanTitle = metadata.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let cleanBody = metadata.body.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let notificationKey = metadata.notificationKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = cleanTitle.isEmpty ? (cleanAppName.isEmpty ? "NotificationSyncFallbackTitle".localized() : cleanAppName) : cleanTitle
|
||||||
|
content.body = cleanBody
|
||||||
|
content.subtitle = cleanAppName.isEmpty ? displaySender : "\(cleanAppName) · \(displaySender)"
|
||||||
|
content.sound = .default
|
||||||
|
content.threadIdentifier = "quickdrop-mirror-\(sanitizeIdentifierComponent(identifierSender))"
|
||||||
|
|
||||||
|
let identifier = makeIdentifier(sender: identifierSender, notificationKey: notificationKey)
|
||||||
|
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
|
||||||
|
center.add(request) { error in
|
||||||
|
if let error = error {
|
||||||
|
log("[MirroredNotificationPresenter] Could not post notification: \(error.localizedDescription)")
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
Settings.sharedInstance.incomingNotificationCount += 1
|
||||||
|
let count = Settings.sharedInstance.incomingNotificationCount
|
||||||
|
log("[MirroredNotificationPresenter] Posted mirrored notification (\(identifier)). Current count: \(count)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeIdentifier(sender: String, notificationKey: String) -> String {
|
||||||
|
let raw = "\(sender)|\(notificationKey)"
|
||||||
|
let digest = SHA256.hash(data: Data(raw.utf8))
|
||||||
|
let prefix = digest.prefix(12).map { String(format: "%02x", $0) }.joined()
|
||||||
|
return "quickdrop-mirror-\(prefix)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sanitizeIdentifierComponent(_ value: String) -> String {
|
||||||
|
let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
|
||||||
|
let scalarView = value.unicodeScalars.map { allowed.contains($0) ? Character($0) : "_" }
|
||||||
|
let sanitized = String(scalarView)
|
||||||
|
return sanitized.isEmpty ? "android" : sanitized
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
//
|
||||||
|
// NearbyBonjourServiceResolver.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 19.05.26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Darwin
|
||||||
|
import Foundation
|
||||||
|
import Network
|
||||||
|
|
||||||
|
struct NearbyResolvedBonjourEndpoint {
|
||||||
|
let endpoint: NWEndpoint
|
||||||
|
/// Numeric host string; IPv6 link-local hosts may include a `%scopeID` suffix for NWEndpoint routing.
|
||||||
|
let host: String
|
||||||
|
let port: UInt16
|
||||||
|
let interface: NWInterface?
|
||||||
|
let description: String
|
||||||
|
let isIPv4: Bool
|
||||||
|
let isLinkLocal: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class NearbyBonjourServiceResolver: NSObject, NetServiceDelegate {
|
||||||
|
private static let defaultWatchdogGracePeriod: TimeInterval = 0.5
|
||||||
|
|
||||||
|
private let service: NetService
|
||||||
|
private let resolveTimeout: TimeInterval
|
||||||
|
private let watchdogGracePeriod: TimeInterval
|
||||||
|
private let interface: NWInterface?
|
||||||
|
private var timeoutWorkItem: DispatchWorkItem?
|
||||||
|
private var completion: (([NearbyResolvedBonjourEndpoint]) -> Void)?
|
||||||
|
private var finished = false
|
||||||
|
|
||||||
|
init?(
|
||||||
|
result: NWBrowser.Result,
|
||||||
|
resolveTimeout: TimeInterval,
|
||||||
|
watchdogGracePeriod: TimeInterval = defaultWatchdogGracePeriod,
|
||||||
|
completion: @escaping ([NearbyResolvedBonjourEndpoint]) -> Void
|
||||||
|
) {
|
||||||
|
guard let components = Self.serviceEndpointComponents(from: result) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.service = NetService(
|
||||||
|
domain: Self.normalizedServiceDomain(components.domain),
|
||||||
|
type: Self.normalizedServiceType(components.type),
|
||||||
|
name: components.name
|
||||||
|
)
|
||||||
|
self.resolveTimeout = resolveTimeout
|
||||||
|
self.watchdogGracePeriod = watchdogGracePeriod
|
||||||
|
self.interface = components.interface
|
||||||
|
self.completion = completion
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func serviceEndpointComponents(
|
||||||
|
from result: NWBrowser.Result
|
||||||
|
) -> (name: String, type: String, domain: String, interface: NWInterface?)? {
|
||||||
|
guard case let NWEndpoint.service(name: name, type: type, domain: domain, interface: interface) = result.endpoint else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return (name: name, type: type, domain: domain, interface: interface)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
service.delegate = self
|
||||||
|
service.resolve(withTimeout: resolveTimeout)
|
||||||
|
|
||||||
|
let timeoutWorkItem = DispatchWorkItem { [weak self] in
|
||||||
|
self?.finish(endpoints: [])
|
||||||
|
}
|
||||||
|
self.timeoutWorkItem = timeoutWorkItem
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + resolveTimeout + watchdogGracePeriod, execute: timeoutWorkItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
finish(endpoints: [], notify: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func netServiceDidResolveAddress(_ sender: NetService) {
|
||||||
|
finish(endpoints: Self.resolvedEndpoints(from: sender, interface: interface))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
|
||||||
|
finish(endpoints: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func finish(endpoints: [NearbyResolvedBonjourEndpoint], notify: Bool = true) {
|
||||||
|
guard !finished else { return }
|
||||||
|
finished = true
|
||||||
|
timeoutWorkItem?.cancel()
|
||||||
|
service.stop()
|
||||||
|
service.delegate = nil
|
||||||
|
|
||||||
|
let completion = completion
|
||||||
|
self.completion = nil
|
||||||
|
|
||||||
|
if notify {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion?(endpoints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func resolvedEndpoints(
|
||||||
|
from service: NetService,
|
||||||
|
interface: NWInterface?
|
||||||
|
) -> [NearbyResolvedBonjourEndpoint] {
|
||||||
|
let endpoints = (service.addresses ?? [])
|
||||||
|
.compactMap { resolvedEndpoint(from: $0, interface: interface) }
|
||||||
|
|
||||||
|
var seenDescriptions = Set<String>()
|
||||||
|
let uniqueEndpoints = endpoints.filter { endpoint in
|
||||||
|
seenDescriptions.insert(endpoint.description).inserted
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueEndpoints.sorted { lhs, rhs in
|
||||||
|
if lhs.isIPv4 != rhs.isIPv4 {
|
||||||
|
return lhs.isIPv4
|
||||||
|
}
|
||||||
|
if lhs.isLinkLocal != rhs.isLinkLocal {
|
||||||
|
return !lhs.isLinkLocal
|
||||||
|
}
|
||||||
|
return lhs.description < rhs.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func resolvedEndpoint(
|
||||||
|
from addressData: Data,
|
||||||
|
interface: NWInterface?
|
||||||
|
) -> NearbyResolvedBonjourEndpoint? {
|
||||||
|
addressData.withUnsafeBytes { rawBuffer in
|
||||||
|
guard let baseAddress = rawBuffer.baseAddress else { return nil }
|
||||||
|
|
||||||
|
let sockaddrPointer = baseAddress.assumingMemoryBound(to: sockaddr.self)
|
||||||
|
let addressFamily = Int32(sockaddrPointer.pointee.sa_family)
|
||||||
|
|
||||||
|
guard addressFamily == AF_INET || addressFamily == AF_INET6 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||||
|
var portBuffer = [CChar](repeating: 0, count: Int(NI_MAXSERV))
|
||||||
|
let result = getnameinfo(
|
||||||
|
sockaddrPointer,
|
||||||
|
socklen_t(addressData.count),
|
||||||
|
&hostBuffer,
|
||||||
|
socklen_t(hostBuffer.count),
|
||||||
|
&portBuffer,
|
||||||
|
socklen_t(portBuffer.count),
|
||||||
|
NI_NUMERICHOST | NI_NUMERICSERV
|
||||||
|
)
|
||||||
|
|
||||||
|
guard result == 0 else { return nil }
|
||||||
|
|
||||||
|
var host = String(cString: hostBuffer)
|
||||||
|
if addressFamily == AF_INET6 {
|
||||||
|
host = scopedIPv6Host(host, addressData: addressData)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let portRawValue = UInt16(String(cString: portBuffer)),
|
||||||
|
let port = NWEndpoint.Port(rawValue: portRawValue) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: port)
|
||||||
|
let description: String
|
||||||
|
if addressFamily == AF_INET6 {
|
||||||
|
description = "[\(host)]:\(portRawValue)"
|
||||||
|
} else {
|
||||||
|
description = "\(host):\(portRawValue)"
|
||||||
|
}
|
||||||
|
|
||||||
|
let isIPv6LinkLocal = addressFamily == AF_INET6 && host.lowercased().hasPrefix("fe80:")
|
||||||
|
|
||||||
|
return NearbyResolvedBonjourEndpoint(
|
||||||
|
endpoint: endpoint,
|
||||||
|
host: host,
|
||||||
|
port: portRawValue,
|
||||||
|
interface: isIPv6LinkLocal ? interface : nil,
|
||||||
|
description: description,
|
||||||
|
isIPv4: addressFamily == AF_INET,
|
||||||
|
isLinkLocal: isIPv6LinkLocal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func scopedIPv6Host(
|
||||||
|
_ host: String,
|
||||||
|
addressData: Data
|
||||||
|
) -> String {
|
||||||
|
guard !host.contains("%"),
|
||||||
|
host.lowercased().hasPrefix("fe80:") else {
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let scopeID = ipv6ScopeID(from: addressData), scopeID != 0 else {
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(host)%\(scopeID)"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func ipv6ScopeID(from addressData: Data) -> UInt32? {
|
||||||
|
addressData.withUnsafeBytes { rawBuffer in
|
||||||
|
guard let baseAddress = rawBuffer.baseAddress else { return nil }
|
||||||
|
let sockaddrPointer = baseAddress.assumingMemoryBound(to: sockaddr.self)
|
||||||
|
guard Int32(sockaddrPointer.pointee.sa_family) == AF_INET6 else { return nil }
|
||||||
|
return baseAddress.assumingMemoryBound(to: sockaddr_in6.self).pointee.sin6_scope_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func normalizedServiceDomain(_ domain: String) -> String {
|
||||||
|
let trimmedDomain = domain.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmedDomain.isEmpty else { return "local." }
|
||||||
|
return trimmedDomain.hasSuffix(".") ? trimmedDomain : "\(trimmedDomain)."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func normalizedServiceType(_ type: String) -> String {
|
||||||
|
let trimmedType = type.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return trimmedType.hasSuffix(".") ? trimmedType : "\(trimmedType)."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,8 +29,13 @@ class NearbyConnection {
|
|||||||
var cancelled: Bool = false
|
var cancelled: Bool = false
|
||||||
var isTransferring: Bool = false {
|
var isTransferring: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
log("[NearbyConnection \(self.id)] Now transferring. Setting up data transfer inactivity timer.")
|
dataTransferInactivityTimer?.cancel()
|
||||||
startDataTransferTimer(previousBytesTransferred: bytesTransferred)
|
dataTransferInactivityTimer = nil
|
||||||
|
|
||||||
|
if isTransferring {
|
||||||
|
log("[NearbyConnection \(self.id)] Now transferring. Setting up data transfer inactivity timer.")
|
||||||
|
startDataTransferTimer(previousBytesTransferred: bytesTransferred)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,9 +43,10 @@ class NearbyConnection {
|
|||||||
private var connectionClosed: Bool = false
|
private var connectionClosed: Bool = false
|
||||||
|
|
||||||
private var inactivityTimer: DispatchSourceTimer?
|
private var inactivityTimer: DispatchSourceTimer?
|
||||||
|
private var dataTransferInactivityTimer: DispatchSourceTimer?
|
||||||
|
|
||||||
private let ackInterval: TimeInterval = 5
|
private let ackInterval: TimeInterval = 5
|
||||||
private let ackTimeoutInterval: TimeInterval = 30
|
private let ackTimeoutInterval: TimeInterval = 60
|
||||||
private var lastKeepAliveFrameSent = Date()
|
private var lastKeepAliveFrameSent = Date()
|
||||||
|
|
||||||
// UKEY2-related state
|
// UKEY2-related state
|
||||||
@@ -189,7 +195,14 @@ class NearbyConnection {
|
|||||||
}
|
}
|
||||||
guard let content = content else {
|
guard let content = content else {
|
||||||
log("[NearbyConnection \(self.id)] Received nil content during receiveFrameAsync(). IsComplete: \(isComplete)")
|
log("[NearbyConnection \(self.id)] Received nil content during receiveFrameAsync(). IsComplete: \(isComplete)")
|
||||||
assertionFailure()
|
self.lastError = NearbyError.protocolError("Received empty frame header")
|
||||||
|
self.protocolError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard content.count == 4 else {
|
||||||
|
log("[NearbyConnection \(self.id)] Received invalid frame header size \(content.count), expected 4")
|
||||||
|
self.lastError = NearbyError.protocolError("Unexpected frame header length")
|
||||||
|
self.protocolError()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let frameLength = UInt32(content[0]) << 24 | UInt32(content[1]) << 16 | UInt32(content[2]) << 8 | UInt32(content[3])
|
let frameLength = UInt32(content[0]) << 24 | UInt32(content[1]) << 16 | UInt32(content[2]) << 8 | UInt32(content[3])
|
||||||
@@ -206,7 +219,7 @@ class NearbyConnection {
|
|||||||
|
|
||||||
|
|
||||||
private func receiveFrameAsync(length: UInt32) {
|
private func receiveFrameAsync(length: UInt32) {
|
||||||
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { [self] content, _, isComplete, _ in
|
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { [self] content, _, isComplete, error in
|
||||||
if self.connectionClosed {
|
if self.connectionClosed {
|
||||||
log("[NearbyConnection \(self.id)] Received FIN from peer, connection closed.")
|
log("[NearbyConnection \(self.id)] Received FIN from peer, connection closed.")
|
||||||
connection.cancel()
|
connection.cancel()
|
||||||
@@ -217,6 +230,13 @@ class NearbyConnection {
|
|||||||
|
|
||||||
if let content = content, !content.isEmpty {
|
if let content = content, !content.isEmpty {
|
||||||
log("[NearbyConnection \(self.id)] Connection closed by peer during receiveFrameAsync(length:), and frame not empty. Frame length: \(content.count)")
|
log("[NearbyConnection \(self.id)] Connection closed by peer during receiveFrameAsync(length:), and frame not empty. Frame length: \(content.count)")
|
||||||
|
|
||||||
|
if content.count == Int(length) {
|
||||||
|
self.processReceivedFrame(frameData: content)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log("[NearbyConnection \(self.id)] Dropping trailing partial frame during connection close. Expected \(length) bytes, got \(content.count)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log("[NearbyConnection \(self.id)] Connection closed by peer during receiveFrameAsync(length:), but no content received anymore (\(String(describing: content))).")
|
log("[NearbyConnection \(self.id)] Connection closed by peer during receiveFrameAsync(length:), but no content received anymore (\(String(describing: content))).")
|
||||||
@@ -230,11 +250,23 @@ class NearbyConnection {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if let error = error {
|
||||||
|
log("[NearbyConnection \(self.id)] Error during receiveFrameAsync(length:): \(error)")
|
||||||
|
self.lastError = error
|
||||||
|
self.protocolError()
|
||||||
|
return
|
||||||
|
}
|
||||||
guard let content = content else {
|
guard let content = content else {
|
||||||
log("[NearbyConnection \(self.id)] Received nil content during receiveFrameAsync(length:). IsComplete: \(isComplete)")
|
log("[NearbyConnection \(self.id)] Received nil content during receiveFrameAsync(length:). IsComplete: \(isComplete)")
|
||||||
self.protocolError()
|
self.protocolError()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
guard content.count == Int(length) else {
|
||||||
|
log("[NearbyConnection \(self.id)] Dropping partial frame body. Expected \(length) bytes, got \(content.count)")
|
||||||
|
self.lastError = NearbyError.protocolError("Unexpected frame body length")
|
||||||
|
self.protocolError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.startAndResetHeartbeatTimer()
|
self.startAndResetHeartbeatTimer()
|
||||||
self.processReceivedFrame(frameData: content)
|
self.processReceivedFrame(frameData: content)
|
||||||
@@ -256,7 +288,10 @@ class NearbyConnection {
|
|||||||
UInt8(truncatingIfNeeded: length),
|
UInt8(truncatingIfNeeded: length),
|
||||||
])
|
])
|
||||||
lengthPrefixedData.append(frame)
|
lengthPrefixedData.append(frame)
|
||||||
connection.send(content: lengthPrefixedData, completion: .contentProcessed { _ in
|
connection.send(content: lengthPrefixedData, completion: .contentProcessed { error in
|
||||||
|
if let error = error {
|
||||||
|
log("[NearbyConnection \(self.id)] Error while sending frame: \(error)")
|
||||||
|
}
|
||||||
if let completion = completion {
|
if let completion = completion {
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
@@ -367,9 +402,17 @@ class NearbyConnection {
|
|||||||
let d2dMsg = try Securegcm_DeviceToDeviceMessage(serializedBytes: decryptedData)
|
let d2dMsg = try Securegcm_DeviceToDeviceMessage(serializedBytes: decryptedData)
|
||||||
|
|
||||||
guard d2dMsg.hasMessage, d2dMsg.hasSequenceNumber else { throw NearbyError.requiredFieldMissing("d2dMessage.message|sequenceNumber") }
|
guard d2dMsg.hasMessage, d2dMsg.hasSequenceNumber else { throw NearbyError.requiredFieldMissing("d2dMessage.message|sequenceNumber") }
|
||||||
clientSeq += 1
|
let receivedSeq = d2dMsg.sequenceNumber
|
||||||
|
let expectedSeq = clientSeq + 1
|
||||||
|
|
||||||
guard d2dMsg.sequenceNumber == clientSeq else { throw NearbyError.protocolError("Wrong sequence number. Expected \(clientSeq), got \(d2dMsg.sequenceNumber)") }
|
if receivedSeq == expectedSeq {
|
||||||
|
clientSeq = receivedSeq
|
||||||
|
} else if receivedSeq <= clientSeq {
|
||||||
|
log("[NearbyConnection \(self.id)] Ignoring duplicate/stale frame sequence \(receivedSeq). Current sequence is \(clientSeq)")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw NearbyError.protocolError("Wrong sequence number. Expected \(expectedSeq), got \(receivedSeq)")
|
||||||
|
}
|
||||||
let offlineFrame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: d2dMsg.message)
|
let offlineFrame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: d2dMsg.message)
|
||||||
|
|
||||||
if offlineFrame.hasV1, offlineFrame.v1.hasType, case .payloadTransfer = offlineFrame.v1.type {
|
if offlineFrame.hasV1, offlineFrame.v1.hasType, case .payloadTransfer = offlineFrame.v1.type {
|
||||||
@@ -381,14 +424,29 @@ class NearbyConnection {
|
|||||||
let chunk = payloadTransfer.payloadChunk
|
let chunk = payloadTransfer.payloadChunk
|
||||||
|
|
||||||
guard header.hasType, header.hasID else { throw NearbyError.requiredFieldMissing("payloadHeader.type|id") }
|
guard header.hasType, header.hasID else { throw NearbyError.requiredFieldMissing("payloadHeader.type|id") }
|
||||||
guard payloadTransfer.hasPayloadChunk, chunk.hasOffset, chunk.hasFlags else {
|
|
||||||
|
if payloadTransfer.hasPayloadChunk {
|
||||||
if payloadTransfer.controlMessage.event == .payloadCanceled {
|
guard chunk.hasOffset, chunk.hasFlags else {
|
||||||
|
log("[NearbyConnection \(self.id)] Payload transfer chunk is missing offset or flags. Frame is \(payloadTransfer.debugDescription)")
|
||||||
|
throw NearbyError.requiredFieldMissing("payloadChunk.offset|flags")
|
||||||
|
}
|
||||||
|
} else if payloadTransfer.hasControlMessage || payloadTransfer.packetType == .control {
|
||||||
|
switch payloadTransfer.controlMessage.event {
|
||||||
|
case .payloadCanceled:
|
||||||
log("[NearbyConnection \(self.id)] Cancel control frame received.")
|
log("[NearbyConnection \(self.id)] Cancel control frame received.")
|
||||||
throw NearbyError.canceled(reason: .userCanceled)
|
throw NearbyError.canceled(reason: .userCanceled)
|
||||||
|
case .payloadReceivedAck:
|
||||||
|
// Legacy peers can send ACKs as control events instead of packetType=.payloadAck.
|
||||||
|
return
|
||||||
|
case .payloadError:
|
||||||
|
throw NearbyError.protocolError("Peer reported payload error")
|
||||||
|
case .unknownEventType:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
} else if payloadTransfer.packetType == .payloadAck {
|
||||||
log("[NearbyConnection \(self.id)] Payload transfer chunk is missing offset or flags. Frame is \(payloadTransfer.debugDescription)")
|
return
|
||||||
|
} else {
|
||||||
|
log("[NearbyConnection \(self.id)] Payload transfer frame missing supported payload content. Frame is \(payloadTransfer.debugDescription)")
|
||||||
throw NearbyError.requiredFieldMissing("payloadChunk.offset|flags")
|
throw NearbyError.requiredFieldMissing("payloadChunk.offset|flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +490,18 @@ class NearbyConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .keepAlive = offlineFrame.v1.type {
|
else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .keepAlive = offlineFrame.v1.type {
|
||||||
sendKeepAlive(ack: true)
|
if offlineFrame.v1.hasKeepAlive {
|
||||||
|
let keepAlive = offlineFrame.v1.keepAlive
|
||||||
|
if !keepAlive.hasAck || !keepAlive.ack {
|
||||||
|
sendKeepAlive(ack: true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Older peers can omit the keepAlive payload; default to ACK reply.
|
||||||
|
sendKeepAlive(ack: true)
|
||||||
|
}
|
||||||
|
} else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .disconnection = offlineFrame.v1.type {
|
||||||
|
log("[NearbyConnection \(self.id)] Received disconnection frame.")
|
||||||
|
disconnect()
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if offlineFrame.hasV1, offlineFrame.v1.hasType, offlineFrame.v1.type == .bandwidthUpgradeRetry {
|
if offlineFrame.hasV1, offlineFrame.v1.hasType, offlineFrame.v1.type == .bandwidthUpgradeRetry {
|
||||||
@@ -466,17 +535,12 @@ class NearbyConnection {
|
|||||||
guard peerKey.hasEcP256PublicKey else { throw NearbyError.requiredFieldMissing("peerKey.ecP256PublicKey") }
|
guard peerKey.hasEcP256PublicKey else { throw NearbyError.requiredFieldMissing("peerKey.ecP256PublicKey") }
|
||||||
|
|
||||||
let domain = Domain.instance(curve: .EC256r1)
|
let domain = Domain.instance(curve: .EC256r1)
|
||||||
var clientX = peerKey.ecP256PublicKey.x
|
let clientX = peerKey.ecP256PublicKey.x.ensureLength(length: 32)
|
||||||
var clientY = peerKey.ecP256PublicKey.y
|
let clientY = peerKey.ecP256PublicKey.y.ensureLength(length: 32)
|
||||||
if clientX.count > 32 {
|
|
||||||
clientX = clientX.suffix(32)
|
|
||||||
}
|
|
||||||
if clientY.count > 32 {
|
|
||||||
clientY = clientY.suffix(32)
|
|
||||||
}
|
|
||||||
let key = try ECPublicKey(domain: domain, w: Point(BInt(magnitude: [UInt8](clientX)), BInt(magnitude: [UInt8](clientY))))
|
let key = try ECPublicKey(domain: domain, w: Point(BInt(magnitude: [UInt8](clientX)), BInt(magnitude: [UInt8](clientY))))
|
||||||
|
|
||||||
let dhs = try (privateKey?.domain.multiplyPoint(key.w, privateKey!.s).x.asMagnitudeBytes())!
|
let dhsRaw = try (privateKey?.domain.multiplyPoint(key.w, privateKey!.s).x.asMagnitudeBytes())!
|
||||||
|
let dhs = Data(dhsRaw).ensureLength(length: 32)
|
||||||
var sha = SHA256()
|
var sha = SHA256()
|
||||||
sha.update(data: dhs)
|
sha.update(data: dhs)
|
||||||
let derivedSecretKey = Data(sha.finalize())
|
let derivedSecretKey = Data(sha.finalize())
|
||||||
@@ -539,6 +603,8 @@ class NearbyConnection {
|
|||||||
connection.stateUpdateHandler = nil
|
connection.stateUpdateHandler = nil
|
||||||
inactivityTimer?.cancel()
|
inactivityTimer?.cancel()
|
||||||
inactivityTimer = nil
|
inactivityTimer = nil
|
||||||
|
dataTransferInactivityTimer?.cancel()
|
||||||
|
dataTransferInactivityTimer = nil
|
||||||
connectionClosed = true
|
connectionClosed = true
|
||||||
connection.send(content: nil, isComplete: true, completion: .contentProcessed { _ in })
|
connection.send(content: nil, isComplete: true, completion: .contentProcessed { _ in })
|
||||||
}
|
}
|
||||||
@@ -549,15 +615,23 @@ class NearbyConnection {
|
|||||||
offlineFrame.version = .v1
|
offlineFrame.version = .v1
|
||||||
offlineFrame.v1.type = .disconnection
|
offlineFrame.v1.type = .disconnection
|
||||||
offlineFrame.v1.disconnection = Location_Nearby_Connections_DisconnectionFrame()
|
offlineFrame.v1.disconnection = Location_Nearby_Connections_DisconnectionFrame()
|
||||||
|
|
||||||
if encryptionDone {
|
if connectionClosed {
|
||||||
try encryptAndSendOfflineFrame(offlineFrame)
|
disconnect()
|
||||||
} else {
|
return
|
||||||
try sendFrameAsync(offlineFrame.serializedData())
|
}
|
||||||
|
|
||||||
|
let finishDisconnect: () -> Void = {
|
||||||
|
log("[NearbyConnection \(self.id)] Sent disconnection frame during sendDisconnectionAndDisconnect")
|
||||||
|
self.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
log("[NearbyConnection \(self.id)] Sent disconnection frame during sendDisconnectionAndDisconnect")
|
if encryptionDone {
|
||||||
disconnect()
|
try encryptAndSendOfflineFrame(offlineFrame, completion: finishDisconnect)
|
||||||
|
} else {
|
||||||
|
let data = try offlineFrame.serializedData()
|
||||||
|
sendFrameAsync(data, completion: finishDisconnect)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -597,8 +671,11 @@ class NearbyConnection {
|
|||||||
|
|
||||||
|
|
||||||
func startAndResetHeartbeatTimer() {
|
func startAndResetHeartbeatTimer() {
|
||||||
|
if connectionClosed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if lastKeepAliveFrameSent.isOlderThan(seconds: ackInterval) {
|
if encryptionDone && lastKeepAliveFrameSent.isOlderThan(seconds: ackInterval) {
|
||||||
sendKeepAlive(ack: false)
|
sendKeepAlive(ack: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,7 +700,14 @@ class NearbyConnection {
|
|||||||
|
|
||||||
|
|
||||||
func startDataTransferTimer(previousBytesTransferred: Int64) {
|
func startDataTransferTimer(previousBytesTransferred: Int64) {
|
||||||
NearbyConnection.dispatchQueue.asyncAfter(deadline: .now() + ackTimeoutInterval) {
|
dataTransferInactivityTimer?.cancel()
|
||||||
|
|
||||||
|
dataTransferInactivityTimer = DispatchSource.makeTimerSource(queue: NearbyConnection.dispatchQueue)
|
||||||
|
dataTransferInactivityTimer?.schedule(deadline: .now() + ackTimeoutInterval)
|
||||||
|
dataTransferInactivityTimer?.setEventHandler { [weak self] in
|
||||||
|
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard !self.connectionClosed, self.isTransferring else { return }
|
||||||
|
|
||||||
if previousBytesTransferred < self.bytesTransferred {
|
if previousBytesTransferred < self.bytesTransferred {
|
||||||
// everything good, transferred more than last check, schedule next check
|
// everything good, transferred more than last check, schedule next check
|
||||||
@@ -631,13 +715,13 @@ class NearbyConnection {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// connection stale, need to abort
|
// connection stale, need to abort
|
||||||
if !self.connectionClosed {
|
log("[NearbyConnection \(self.id)] Connection timeout: No more data received in \(self.ackTimeoutInterval) seconds")
|
||||||
log("[NearbyConnection \(self.id)] Connection timeout: No more data received in \(self.ackTimeoutInterval) seconds")
|
self.lastError = NearbyError.canceled(reason: .timedOut)
|
||||||
self.lastError = NearbyError.canceled(reason: .timedOut)
|
self.disconnect()
|
||||||
self.disconnect()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataTransferInactivityTimer?.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import LUI
|
|||||||
|
|
||||||
public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate, ObservableObject {
|
public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate, ObservableObject {
|
||||||
|
|
||||||
|
// MARK: Private Properties
|
||||||
|
|
||||||
#if !EXTENSION
|
#if !EXTENSION
|
||||||
private let sleepManager = SleepManager.shared
|
private let sleepManager = SleepManager.shared
|
||||||
#endif
|
#endif
|
||||||
@@ -31,9 +33,14 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
private var outboundAppDelegates: [OutboundAppDelegate] = []
|
private var outboundAppDelegates: [OutboundAppDelegate] = []
|
||||||
private var inboundAppDelegates: [InboundAppDelegate] = []
|
private var inboundAppDelegates: [InboundAppDelegate] = []
|
||||||
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
|
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
|
||||||
|
private var pendingOutgoingTransferPreparations: [String: UUID] = [:]
|
||||||
|
private var outgoingServiceResolvers: [String: NearbyBonjourServiceResolver] = [:]
|
||||||
|
private let pendingNotificationSyncTrustQueue = DispatchQueue(label: "NearbyConnectionManager.pendingNotificationSyncTrust")
|
||||||
|
private var pendingNotificationSyncTrust: [String: PendingReceiverTrust] = [:]
|
||||||
private var startedDeviceDiscovery = false
|
private var startedDeviceDiscovery = false
|
||||||
private var startedAdvertising = false
|
private var startedAdvertising = false { didSet { informAboutStatus() }}
|
||||||
private var browsers: [NWBrowser] = []
|
private var browsers: [NWBrowser] = []
|
||||||
|
private let deviceNameResolver = NearbyDeviceNameResolver()
|
||||||
private let serviceTypes = ["_FC9F5ED42C8A._tcp."]
|
private let serviceTypes = ["_FC9F5ED42C8A._tcp."]
|
||||||
private var qrCodePrivateKey: ECPrivateKey?
|
private var qrCodePrivateKey: ECPrivateKey?
|
||||||
private var qrCodeAdvertisingToken: Data?
|
private var qrCodeAdvertisingToken: Data?
|
||||||
@@ -42,24 +49,37 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
private let connectionMonitorQueue = DispatchQueue(label: "NetworkConnectionMonitorQueue")
|
private let connectionMonitorQueue = DispatchQueue(label: "NetworkConnectionMonitorQueue")
|
||||||
private var securityScopeUrl: URL?
|
private var securityScopeUrl: URL?
|
||||||
private static let customDeviceNameKey = "com.leonboettger.quickdrop.deviceName"
|
private static let customDeviceNameKey = "com.leonboettger.quickdrop.deviceName"
|
||||||
|
private var defaultPort: NWEndpoint.Port = 50362
|
||||||
|
private let txtKeyEndpointInfo = "n"
|
||||||
|
private let txtQuickDropCapabilities = "qd_caps"
|
||||||
|
private let txtQuickDropDeviceKeyFingerprint = "qd_kid"
|
||||||
|
private let notificationSyncCapabilityToken = "nsync1"
|
||||||
|
private let clipboardReceiveCapabilityToken = "cliprecv1"
|
||||||
|
private let unnamedAndroidFallbackDeviceName = "AndroidDevice".localized()
|
||||||
|
|
||||||
public let endpointID: [UInt8] = generateEndpointID()
|
|
||||||
|
// MARK: Shared Instance
|
||||||
|
|
||||||
public static let shared = NearbyConnectionManager()
|
public static let shared = NearbyConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Published State
|
||||||
|
|
||||||
@Published var attachments: AttachmentDetails? = nil
|
@Published var attachments: AttachmentDetails? = nil
|
||||||
@Published var hasLocalNetworkPermission = true
|
@Published var hasLocalNetworkPermission = true
|
||||||
@Published var isConnectedToLocalNetwork = true
|
@Published var isConnectedToLocalNetwork = true { didSet { informAboutStatus() }}
|
||||||
@Published var deviceInfo: EndpointInfo
|
@Published var deviceInfo: EndpointInfo
|
||||||
|
|
||||||
public var connectionUpdateCallback: (Bool) -> Void = { _ in } {
|
|
||||||
didSet {
|
|
||||||
connectionUpdateCallback(isConnectedToLocalNetwork)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// MARK: Public Properties
|
||||||
|
|
||||||
|
public var endpointID: [UInt8] = getEndpointID(forceRegeneration: false)
|
||||||
|
public var connectionUpdateCallback: (Bool) -> Void = { _ in } { didSet { informAboutStatus() }}
|
||||||
public var changedDeviceNameCallback: () -> Void = { }
|
public var changedDeviceNameCallback: () -> Void = { }
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Initializers
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.deviceInfo = Self.getEndpointInfo()
|
self.deviceInfo = Self.getEndpointInfo()
|
||||||
tcpListener = try! NWListener(using: NWParameters(tls: .none))
|
tcpListener = try! NWListener(using: NWParameters(tls: .none))
|
||||||
@@ -83,7 +103,6 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
log("[NearbyConnectionManager] Local network lost.")
|
log("[NearbyConnectionManager] Local network lost.")
|
||||||
}
|
}
|
||||||
self.isConnectedToLocalNetwork = isConnected
|
self.isConnectedToLocalNetwork = isConnected
|
||||||
self.connectionUpdateCallback(isConnected)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +132,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Deinitializer
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.stopAccessingSaveDirectory()
|
self.stopAccessingSaveDirectory()
|
||||||
}
|
}
|
||||||
@@ -161,22 +182,73 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func becomeVisible() {
|
public func becomeVisible(randomPort: Bool = false) {
|
||||||
if startedAdvertising {
|
if startedAdvertising {
|
||||||
log("[NearbyConnectionManager] Already advertising, skipping")
|
log("[NearbyConnectionManager] Already advertising, skipping")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log("[NearbyConnectionManager] Becoming visible")
|
log("[NearbyConnectionManager] Becoming visible")
|
||||||
|
let parameters = NWParameters(tls: .none)
|
||||||
|
|
||||||
tcpListener = try! NWListener(using: NWParameters(tls: .none))
|
do {
|
||||||
startedAdvertising = true
|
if randomPort {
|
||||||
|
self.tcpListener = try NWListener(using: parameters)
|
||||||
tcpListener.stateUpdateHandler = { (state: NWListener.State) in
|
} else {
|
||||||
if case .ready = state {
|
self.tcpListener = try NWListener(using: parameters, on: defaultPort)
|
||||||
self.initMDNS()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
log("[NearbyConnectionManager] Error starting TCP listener: \(error). Trying with random port.")
|
||||||
|
|
||||||
|
runAfter(seconds: 1) {
|
||||||
|
self.becomeVisible(randomPort: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
startedAdvertising = true
|
||||||
|
|
||||||
|
tcpListener.stateUpdateHandler = { state in
|
||||||
|
switch state {
|
||||||
|
case .ready:
|
||||||
|
log("[NearbyConnectionManager] Listener ready")
|
||||||
|
self.initMDNS(forceIDRegeneration: randomPort)
|
||||||
|
|
||||||
|
if let port = self.tcpListener.port, port != self.defaultPort {
|
||||||
|
log("[NearbyConnectionManager] Updated default port to \(port)")
|
||||||
|
self.defaultPort = port
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failed(let error):
|
||||||
|
log("[NearbyConnectionManager] Listener failed: \(error)")
|
||||||
|
self.becomeInvisible()
|
||||||
|
|
||||||
|
runAfter(seconds: 1) {
|
||||||
|
if case .posix(let posixError) = error,
|
||||||
|
posixError == .EADDRINUSE {
|
||||||
|
self.becomeVisible(randomPort: true)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.becomeVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .cancelled:
|
||||||
|
log("[NearbyConnectionManager] Listener cancelled")
|
||||||
|
|
||||||
|
case .setup:
|
||||||
|
log("[NearbyConnectionManager] Listener setup")
|
||||||
|
|
||||||
|
case .waiting(let state):
|
||||||
|
log("[NearbyConnectionManager] Listener waiting: \(state)")
|
||||||
|
|
||||||
|
@unknown default:
|
||||||
|
log("[NearbyConnectionManager] Listener unknown state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tcpListener.newConnectionHandler = { (connection: NWConnection) in
|
tcpListener.newConnectionHandler = { (connection: NWConnection) in
|
||||||
let id = UUID().uuidString
|
let id = UUID().uuidString
|
||||||
let conn = InboundNearbyConnection(connection: connection, id: id)
|
let conn = InboundNearbyConnection(connection: connection, id: id)
|
||||||
@@ -184,6 +256,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
conn.delegate = self
|
conn.delegate = self
|
||||||
conn.start()
|
conn.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
tcpListener.start(queue: .global(qos: .utility))
|
tcpListener.start(queue: .global(qos: .utility))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,11 +275,13 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static func generateEndpointID() -> [UInt8] {
|
private static func getEndpointID(forceRegeneration: Bool) -> [UInt8] {
|
||||||
|
|
||||||
// Try to retrieve from UserDefaults
|
// Try to retrieve from UserDefaults
|
||||||
if let savedString = Settings.shared.endpointID,
|
if !forceRegeneration,
|
||||||
|
let savedString = Settings.sharedInstance.endpointID,
|
||||||
let savedData = savedString.data(using: .utf8) {
|
let savedData = savedString.data(using: .utf8) {
|
||||||
|
log("[NearbyConnectionManager] Using cached endpoint ID: \(savedString)")
|
||||||
return [UInt8](savedData)
|
return [UInt8](savedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,13 +294,17 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
|
|
||||||
// Save to UserDefaults as String
|
// Save to UserDefaults as String
|
||||||
let idString = String(bytes: id, encoding: .utf8) ?? ""
|
let idString = String(bytes: id, encoding: .utf8) ?? ""
|
||||||
Settings.shared.endpointID = idString
|
Settings.sharedInstance.endpointID = idString
|
||||||
|
log("[NearbyConnectionManager] Storing new endpoint ID: \(idString) ======================================")
|
||||||
|
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func initMDNS() {
|
private func initMDNS(forceIDRegeneration: Bool) {
|
||||||
|
|
||||||
|
self.endpointID = Self.getEndpointID(forceRegeneration: forceIDRegeneration)
|
||||||
|
|
||||||
let nameBytes: [UInt8] = [
|
let nameBytes: [UInt8] = [
|
||||||
0x23, // PCP
|
0x23, // PCP
|
||||||
endpointID[0], endpointID[1], endpointID[2], endpointID[3],
|
endpointID[0], endpointID[1], endpointID[2], endpointID[3],
|
||||||
@@ -239,14 +318,33 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
mdnsServices = serviceTypes.map { serviceType in
|
mdnsServices = serviceTypes.map { serviceType in
|
||||||
let service = NetService(domain: "", type: serviceType, name: name, port: port)
|
let service = NetService(domain: "", type: serviceType, name: name, port: port)
|
||||||
service.delegate = self
|
service.delegate = self
|
||||||
service.setTXTRecord(NetService.data(fromTXTRecord: [
|
var txtRecord: [String: Data] = [
|
||||||
"n": deviceInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!,
|
txtKeyEndpointInfo: deviceInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!,
|
||||||
]))
|
]
|
||||||
|
if let keyFingerprint = notificationSyncKeyFingerprintHex() {
|
||||||
|
txtRecord[txtQuickDropDeviceKeyFingerprint] = Data(keyFingerprint.utf8)
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
let capabilityTokens = [
|
||||||
|
notificationSyncCapabilityToken,
|
||||||
|
clipboardReceiveCapabilityToken,
|
||||||
|
]
|
||||||
|
txtRecord[txtQuickDropCapabilities] = Data(capabilityTokens.joined(separator: ",").utf8)
|
||||||
|
#endif
|
||||||
|
service.setTXTRecord(NetService.data(fromTXTRecord: txtRecord))
|
||||||
service.publish()
|
service.publish()
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func notificationSyncKeyFingerprintHex() -> String? {
|
||||||
|
guard let keyID = IdentityManager.shared.getPublicKey()?.toGenericPublicKey().id() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return keyID.hex.lowercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func stopMDNS() {
|
private func stopMDNS() {
|
||||||
// Gracefully stop each published service
|
// Gracefully stop each published service
|
||||||
@@ -273,13 +371,32 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
|
|
||||||
func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?) {
|
func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?) {
|
||||||
incomingConnections.removeValue(forKey: connection.id)
|
incomingConnections.removeValue(forKey: connection.id)
|
||||||
|
|
||||||
|
// Control transfers suppress normal success callbacks, but
|
||||||
|
// still forward failures so pending consent/error UI can be cleaned up.
|
||||||
|
if connection.isControlTransfer, error == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !connection.wasUserRejected {
|
if !connection.wasUserRejected {
|
||||||
inboundAppDelegates.forEach { delegate in
|
inboundAppDelegates.forEach { delegate in
|
||||||
delegate.connectionWasTerminated(connectionID: connection.id, from: connection.remoteDeviceInfo, savedFiles: savedFiles, wasPlainTextTransfer: connection.isPlainTextTransfer, error: error)
|
delegate.connectionWasTerminated(
|
||||||
|
connectionID: connection.id,
|
||||||
|
from: connection.remoteDeviceInfo,
|
||||||
|
savedFiles: savedFiles,
|
||||||
|
wasPlainTextTransfer: connection.isPlainTextTransfer,
|
||||||
|
wasClipboardSync: connection.isClipboardSyncTransfer,
|
||||||
|
error: error
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pairingSetupConfirmed(connection _: InboundNearbyConnection) {
|
||||||
|
inboundAppDelegates.forEach { delegate in
|
||||||
|
delegate.pairingSetupConfirmed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func showPlusScreen() {
|
func showPlusScreen() {
|
||||||
@@ -296,11 +413,16 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func submitUserConsent(transferID: String, accept: Bool, trustDevice: Bool) {
|
public func submitUserConsent(
|
||||||
|
transferID: String,
|
||||||
|
accept: Bool,
|
||||||
|
trustDevice: Bool,
|
||||||
|
pairingToken: String? = nil
|
||||||
|
) {
|
||||||
guard let conn = incomingConnections[transferID] else { return }
|
guard let conn = incomingConnections[transferID] else { return }
|
||||||
|
|
||||||
log("[NearbyConnectionManager] Submitting user consent for transfer \(transferID), accepted: \(accept)")
|
log("[NearbyConnectionManager] Submitting user consent for transfer \(transferID), accepted: \(accept)")
|
||||||
conn.submitUserConsent(accepted: accept, trustDevice: trustDevice)
|
conn.submitUserConsent(accepted: accept, trustDevice: trustDevice, pairingToken: pairingToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -324,6 +446,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
self.addFoundDevice(service: res)
|
self.addFoundDevice(service: res)
|
||||||
case let .removed(res):
|
case let .removed(res):
|
||||||
self.removeFoundDevice(service: res)
|
self.removeFoundDevice(service: res)
|
||||||
|
case let .changed(_, new, _):
|
||||||
|
self.addFoundDevice(service: new)
|
||||||
default:
|
default:
|
||||||
log("[NearbyConnectionManager] Ignoring change \(change)")
|
log("[NearbyConnectionManager] Ignoring change \(change)")
|
||||||
}
|
}
|
||||||
@@ -368,7 +492,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
|
|
||||||
browsers.removeAll()
|
browsers.removeAll()
|
||||||
startedDeviceDiscovery = false
|
startedDeviceDiscovery = false
|
||||||
|
deviceNameResolver.cancelAll()
|
||||||
|
|
||||||
for delegate in outboundAppDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
|
|
||||||
@@ -379,6 +503,16 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var isDeviceDiscoveryActive: Bool {
|
||||||
|
startedDeviceDiscovery
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func discoveredDevices() -> [RemoteDeviceInfo] {
|
||||||
|
foundServices.values.compactMap(\.device)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public func addOutboundAppDelegate(_ delegate: OutboundAppDelegate) {
|
public func addOutboundAppDelegate(_ delegate: OutboundAppDelegate) {
|
||||||
@@ -405,16 +539,23 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
|
|
||||||
|
|
||||||
public func cancelTransfer(id: String) {
|
public func cancelTransfer(id: String) {
|
||||||
|
|
||||||
if let transfer = incomingConnections[id] {
|
if let transfer = incomingConnections[id] {
|
||||||
transfer.cancel()
|
transfer.cancel()
|
||||||
}
|
}
|
||||||
|
pendingOutgoingTransferPreparations.removeValue(forKey: id)
|
||||||
|
outgoingServiceResolvers.removeValue(forKey: id)?.cancel()
|
||||||
if let info = outgoingTransfers[id] {
|
if let info = outgoingTransfers[id] {
|
||||||
info.connection.cancel()
|
info.connection.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func cancelAllIncomingConnections() {
|
||||||
|
let incomingTransfers = Array(incomingConnections.values)
|
||||||
|
for transfer in incomingTransfers {
|
||||||
|
transfer.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func endpointID(for service: NWBrowser.Result) -> String? {
|
private func endpointID(for service: NWBrowser.Result) -> String? {
|
||||||
guard case let NWEndpoint.service(name: serviceName, type: _, domain: _, interface: _) = service.endpoint else { return nil }
|
guard case let NWEndpoint.service(name: serviceName, type: _, domain: _, interface: _) = service.endpoint else { return nil }
|
||||||
guard let nameData = Data.dataFromUrlSafeBase64(serviceName) else { return nil }
|
guard let nameData = Data.dataFromUrlSafeBase64(serviceName) else { return nil }
|
||||||
@@ -439,13 +580,58 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
var foundService = FoundServiceInfo(service: service)
|
var foundService = FoundServiceInfo(service: service)
|
||||||
|
|
||||||
guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return }
|
guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return }
|
||||||
guard let endpointInfoEncoded = txtRecord.dictionary["n"] else { return }
|
guard let endpointInfoEncoded = txtRecord.dictionary[txtKeyEndpointInfo] else { return }
|
||||||
guard let endpointInfoData = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return }
|
guard let endpointInfoData = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return }
|
||||||
guard var endpointInfo = EndpointInfo(data: endpointInfoData) else { return }
|
let parsedEndpointInfo = EndpointInfo(data: endpointInfoData)
|
||||||
var deviceInfo: RemoteDeviceInfo?
|
let keyFingerprint = normalizedKeyFingerprint(
|
||||||
|
txtRecord.dictionary[txtQuickDropDeviceKeyFingerprint]
|
||||||
if let _ = endpointInfo.name {
|
)
|
||||||
deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID)
|
let capabilityTokens = Set(
|
||||||
|
txtRecord.dictionary[txtQuickDropCapabilities]?
|
||||||
|
.split(separator: ",")
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { !$0.isEmpty } ?? []
|
||||||
|
)
|
||||||
|
let supportsNotificationSync = capabilityTokens.contains(notificationSyncCapabilityToken)
|
||||||
|
let supportsClipboardReceive = capabilityTokens.contains(clipboardReceiveCapabilityToken)
|
||||||
|
var endpointInfo = parsedEndpointInfo
|
||||||
|
?? EndpointInfo(name: unnamedAndroidFallbackDeviceName, deviceType: .unknown)
|
||||||
|
let isQuickDropPeer = keyFingerprint != nil
|
||||||
|
let needsDeviceNameLookup = deviceNameResolver.needsLookup(for: parsedEndpointInfo)
|
||||||
|
if needsDeviceNameLookup {
|
||||||
|
endpointInfo.name = unnamedAndroidFallbackDeviceName
|
||||||
|
}
|
||||||
|
let deviceInfo = addFoundDevice(
|
||||||
|
foundService: &foundService,
|
||||||
|
endpointInfo: endpointInfo,
|
||||||
|
endpointID: endpointID,
|
||||||
|
isQuickDropPeer: isQuickDropPeer,
|
||||||
|
keyFingerprint: keyFingerprint,
|
||||||
|
supportsNotificationSync: supportsNotificationSync,
|
||||||
|
supportsClipboardReceive: supportsClipboardReceive
|
||||||
|
)
|
||||||
|
|
||||||
|
if needsDeviceNameLookup {
|
||||||
|
deviceNameResolver.resolveName(for: service, endpointID: endpointID) { [weak self] deviceName in
|
||||||
|
guard let self else { return }
|
||||||
|
guard self.foundServices[endpointID]?.device?.name == self.unnamedAndroidFallbackDeviceName else { return }
|
||||||
|
|
||||||
|
var resolvedEndpointInfo = endpointInfo
|
||||||
|
resolvedEndpointInfo.name = deviceName
|
||||||
|
var resolvedFoundService = FoundServiceInfo(service: service)
|
||||||
|
_ = self.addFoundDevice(
|
||||||
|
foundService: &resolvedFoundService,
|
||||||
|
endpointInfo: resolvedEndpointInfo,
|
||||||
|
endpointID: endpointID,
|
||||||
|
isQuickDropPeer: isQuickDropPeer,
|
||||||
|
keyFingerprint: keyFingerprint,
|
||||||
|
supportsNotificationSync: supportsNotificationSync,
|
||||||
|
supportsClipboardReceive: supportsClipboardReceive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
deviceNameResolver.cancelLookup(for: endpointID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let qrData = endpointInfo.qrCodeData, let qrCodeAdvertisingToken = qrCodeAdvertisingToken, let qrCodeNameEncryptionKey = qrCodeNameEncryptionKey {
|
if let qrData = endpointInfo.qrCodeData, let qrCodeAdvertisingToken = qrCodeAdvertisingToken, let qrCodeNameEncryptionKey = qrCodeNameEncryptionKey {
|
||||||
@@ -453,10 +639,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
log("[NearbyConnectionManager] Device has QR data: \(qrData.base64EncodedString()), advertising token is \(qrCodeAdvertisingToken.base64EncodedString())")
|
log("[NearbyConnectionManager] Device has QR data: \(qrData.base64EncodedString()), advertising token is \(qrCodeAdvertisingToken.base64EncodedString())")
|
||||||
|
|
||||||
if qrData == qrCodeAdvertisingToken {
|
if qrData == qrCodeAdvertisingToken {
|
||||||
if let deviceInfo = deviceInfo {
|
for delegate in outboundAppDelegates {
|
||||||
for delegate in outboundAppDelegates {
|
delegate.startTransferWithQrCode(device: deviceInfo)
|
||||||
delegate.startTransferWithQrCode(device: deviceInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if qrData.count > 28 {
|
else if qrData.count > 28 {
|
||||||
@@ -465,7 +649,15 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
let decryptedName = try AES.GCM.open(box, using: qrCodeNameEncryptionKey, authenticating: qrCodeAdvertisingToken)
|
let decryptedName = try AES.GCM.open(box, using: qrCodeNameEncryptionKey, authenticating: qrCodeAdvertisingToken)
|
||||||
guard let name = String.init(data: decryptedName, encoding: .utf8) else { return }
|
guard let name = String.init(data: decryptedName, encoding: .utf8) else { return }
|
||||||
endpointInfo.name = name
|
endpointInfo.name = name
|
||||||
let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID)
|
let deviceInfo = addFoundDevice(
|
||||||
|
foundService: &foundService,
|
||||||
|
endpointInfo: endpointInfo,
|
||||||
|
endpointID: endpointID,
|
||||||
|
isQuickDropPeer: isQuickDropPeer,
|
||||||
|
keyFingerprint: keyFingerprint,
|
||||||
|
supportsNotificationSync: supportsNotificationSync,
|
||||||
|
supportsClipboardReceive: supportsClipboardReceive
|
||||||
|
)
|
||||||
for delegate in outboundAppDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
delegate.startTransferWithQrCode(device: deviceInfo)
|
delegate.startTransferWithQrCode(device: deviceInfo)
|
||||||
}
|
}
|
||||||
@@ -477,22 +669,63 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func addFoundDevice(foundService: inout FoundServiceInfo, endpointInfo: EndpointInfo, endpointID: String) -> RemoteDeviceInfo {
|
private func addFoundDevice(
|
||||||
let deviceInfo = RemoteDeviceInfo(info: endpointInfo, id: endpointID)
|
foundService: inout FoundServiceInfo,
|
||||||
|
endpointInfo: EndpointInfo,
|
||||||
|
endpointID: String,
|
||||||
|
isQuickDropPeer: Bool = false,
|
||||||
|
keyFingerprint: String? = nil,
|
||||||
|
supportsNotificationSync: Bool = false,
|
||||||
|
supportsClipboardReceive: Bool = false
|
||||||
|
) -> RemoteDeviceInfo {
|
||||||
|
let hadPreviousDevice = foundServices[endpointID]?.device != nil
|
||||||
|
let deviceInfo = RemoteDeviceInfo(
|
||||||
|
info: endpointInfo,
|
||||||
|
id: endpointID,
|
||||||
|
isQuickDropPeer: isQuickDropPeer,
|
||||||
|
keyFingerprint: keyFingerprint,
|
||||||
|
supportsNotificationSync: supportsNotificationSync,
|
||||||
|
supportsClipboardReceive: supportsClipboardReceive
|
||||||
|
)
|
||||||
foundService.device = deviceInfo
|
foundService.device = deviceInfo
|
||||||
foundServices[endpointID] = foundService
|
foundServices[endpointID] = foundService
|
||||||
|
|
||||||
|
if hadPreviousDevice {
|
||||||
|
for delegate in outboundAppDelegates {
|
||||||
|
delegate.removeDevice(id: endpointID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for delegate in outboundAppDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
delegate.addDevice(device: deviceInfo)
|
delegate.addDevice(device: deviceInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return deviceInfo
|
return deviceInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func resolveDiscoveredDeviceIdForKeyFingerprint(_ keyFingerprint: String) -> String? {
|
||||||
|
guard let normalizedFingerprint = normalizedKeyFingerprint(keyFingerprint) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundServices.values
|
||||||
|
.first(where: { $0.device?.keyFingerprint == normalizedFingerprint })?
|
||||||
|
.device?
|
||||||
|
.id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func removeFoundDevice(service: NWBrowser.Result) {
|
private func removeFoundDevice(service: NWBrowser.Result) {
|
||||||
guard let endpointID = endpointID(for: service) else { return }
|
guard let endpointID = endpointID(for: service) else { return }
|
||||||
guard let _ = foundServices.removeValue(forKey: endpointID) else { return }
|
deviceNameResolver.cancelLookup(for: endpointID)
|
||||||
|
guard let currentService = foundServices[endpointID] else { return }
|
||||||
|
// Ignore delayed removals for an older Bonjour result after a changed result
|
||||||
|
// has already replaced the service for this endpoint ID.
|
||||||
|
guard serviceIdentity(currentService.service) == serviceIdentity(service) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
foundServices.removeValue(forKey: endpointID)
|
||||||
for delegate in outboundAppDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
delegate.removeDevice(id: endpointID)
|
delegate.removeDevice(id: endpointID)
|
||||||
}
|
}
|
||||||
@@ -505,19 +738,18 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
|
|
||||||
let domain = Domain.instance(curve: .EC256r1)
|
let domain = Domain.instance(curve: .EC256r1)
|
||||||
let (pubKey, privKey) = domain.makeKeyPair()
|
let (pubKey, privKey) = domain.makeKeyPair()
|
||||||
|
|
||||||
qrCodePrivateKey = privKey
|
qrCodePrivateKey = privKey
|
||||||
|
|
||||||
var keyData = Data()
|
var keyData = Data()
|
||||||
keyData.append(contentsOf: [0, 0, 2])
|
keyData.append(contentsOf: [0, 0, 2])
|
||||||
let keyBytes = Data(pubKey.w.x.asSignedBytes())
|
keyData.append(Data(pubKey.w.x.asSignedBytes()).ensureLength(length: 32))
|
||||||
|
|
||||||
// Sometimes, for some keys, there will be a leading zero byte. Strip that, Android really hates it (it breaks the endpoint info)
|
|
||||||
keyData.append(keyBytes.suffixOfAtMost(numBytes: 32))
|
|
||||||
|
|
||||||
let ikm = SymmetricKey(data: keyData)
|
let ikm = SymmetricKey(data: keyData)
|
||||||
|
|
||||||
qrCodeAdvertisingToken = HKDF.deriveKey(ikm: ikm, salt: Data(), info: "advertisingContext".data(using: .utf8)!, outputLength: 16).data()
|
qrCodeAdvertisingToken = HKDF.deriveKey(ikm: ikm, salt: Data(), info: "advertisingContext".data(using: .utf8)!, outputLength: 16).data()
|
||||||
qrCodeNameEncryptionKey = HKDF.deriveKey(ikm: ikm, salt: Data(), info: "encryptionKey".data(using: .utf8)!, outputLength: 16)
|
qrCodeNameEncryptionKey = HKDF.deriveKey(ikm: ikm, salt: Data(), info: "encryptionKey".data(using: .utf8)!, outputLength: 16)
|
||||||
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let qrKey = keyData.urlSafeBase64EncodedString()
|
let qrKey = keyData.urlSafeBase64EncodedString()
|
||||||
let qrCodeImage = try QRCode.build
|
let qrCodeImage = try QRCode.build
|
||||||
@@ -540,29 +772,130 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func startOutgoingTransfer(deviceID: String, delegate: OutboundAppDelegate, urls: [URL], textToSend: String?) {
|
@discardableResult
|
||||||
|
public func startOutgoingTransfer(
|
||||||
|
deviceID: String,
|
||||||
|
delegate: OutboundAppDelegate,
|
||||||
|
urls: [URL],
|
||||||
|
textToSend: String?,
|
||||||
|
preparationFinished: (() -> Void)? = nil,
|
||||||
|
isClipboardSyncTransfer: Bool = false,
|
||||||
|
receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
|
||||||
|
) -> Bool {
|
||||||
log("Starting outgoing transfer to \(deviceID)")
|
log("Starting outgoing transfer to \(deviceID)")
|
||||||
guard let info = foundServices[deviceID] else { return }
|
guard foundServices[deviceID] != nil else {
|
||||||
|
let error = NearbyError.protocolError("TargetDeviceNoLongerNearby".localized())
|
||||||
do {
|
delegate.connectionFailed(error: error)
|
||||||
let localUrls = try saveFilesToTemp(urls: urls)
|
return false
|
||||||
|
}
|
||||||
let tcp = NWProtocolTCP.Options()
|
|
||||||
tcp.noDelay = true
|
let preparationID = UUID()
|
||||||
let nwconn = NWConnection(to: info.service.endpoint, using: NWParameters(tls: .none, tcp: tcp))
|
pendingOutgoingTransferPreparations[deviceID] = preparationID
|
||||||
|
|
||||||
let conn = OutboundNearbyConnection(connection: nwconn, id: deviceID, urlsToSend: localUrls, textToSend: textToSend)
|
DispatchQueue.global(qos: .userInitiated).async { [weak self, weak delegate] in
|
||||||
conn.delegate = self
|
guard let self else { return }
|
||||||
conn.qrCodePrivateKey = qrCodePrivateKey
|
|
||||||
let transfer = OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate)
|
do {
|
||||||
outgoingTransfers[deviceID] = transfer
|
let localUrls = try self.saveFilesToTemp(urls: urls)
|
||||||
conn.start()
|
|
||||||
} catch {
|
DispatchQueue.main.async {
|
||||||
log("[NearbyConnectionManager] Error storing URLs: \(error)")
|
guard let info = self.foundServices[deviceID],
|
||||||
outboundAppDelegates.forEach { delegate in
|
self.pendingOutgoingTransferPreparations[deviceID] == preparationID else {
|
||||||
delegate.connectionFailed(error: error)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let delegate else {
|
||||||
|
self.pendingOutgoingTransferPreparations.removeValue(forKey: deviceID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
preparationFinished?()
|
||||||
|
|
||||||
|
self.resolveOutgoingEndpoint(for: info.service, deviceID: deviceID) { endpoint, requiredInterface in
|
||||||
|
guard let info = self.foundServices[deviceID],
|
||||||
|
self.pendingOutgoingTransferPreparations[deviceID] == preparationID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pendingOutgoingTransferPreparations.removeValue(forKey: deviceID)
|
||||||
|
|
||||||
|
let tcp = NWProtocolTCP.Options()
|
||||||
|
tcp.noDelay = true
|
||||||
|
let parameters = NWParameters(tls: .none, tcp: tcp)
|
||||||
|
parameters.requiredInterface = requiredInterface
|
||||||
|
let nwconn = NWConnection(to: endpoint, using: parameters)
|
||||||
|
|
||||||
|
let conn = OutboundNearbyConnection(
|
||||||
|
connection: nwconn,
|
||||||
|
id: deviceID,
|
||||||
|
urlsToSend: localUrls,
|
||||||
|
textToSend: textToSend,
|
||||||
|
isClipboardSyncTransfer: isClipboardSyncTransfer,
|
||||||
|
receiverAuthenticationPolicy: receiverAuthenticationPolicy
|
||||||
|
)
|
||||||
|
conn.remoteDeviceInfo = info.device
|
||||||
|
conn.delegate = self
|
||||||
|
conn.qrCodePrivateKey = self.qrCodePrivateKey
|
||||||
|
|
||||||
|
let transfer = OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate)
|
||||||
|
self.outgoingTransfers[deviceID] = transfer
|
||||||
|
conn.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log("[NearbyConnectionManager] Error storing URLs: \(error)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard self.pendingOutgoingTransferPreparations[deviceID] == preparationID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pendingOutgoingTransferPreparations.removeValue(forKey: deviceID)
|
||||||
|
delegate?.connectionFailed(error: error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func resolveOutgoingEndpoint(
|
||||||
|
for service: NWBrowser.Result,
|
||||||
|
deviceID: String,
|
||||||
|
completion: @escaping (NWEndpoint, NWInterface?) -> Void
|
||||||
|
) {
|
||||||
|
guard let resolver = NearbyBonjourServiceResolver(result: service, resolveTimeout: 2.5, completion: { [weak self] endpoints in
|
||||||
|
guard let self else { return }
|
||||||
|
self.outgoingServiceResolvers.removeValue(forKey: deviceID)
|
||||||
|
|
||||||
|
if let endpoint = endpoints.first {
|
||||||
|
completion(endpoint.endpoint, endpoint.interface)
|
||||||
|
} else {
|
||||||
|
log("[NearbyConnectionManager] Could not resolve outgoing service for \(deviceID); falling back to Bonjour endpoint \(service.endpoint)")
|
||||||
|
completion(service.endpoint, nil)
|
||||||
|
}
|
||||||
|
}) else {
|
||||||
|
log("[NearbyConnectionManager] Cannot create resolver for outgoing service; falling back to Bonjour endpoint")
|
||||||
|
completion(service.endpoint, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outgoingServiceResolvers.removeValue(forKey: deviceID)?.cancel()
|
||||||
|
outgoingServiceResolvers[deviceID] = resolver
|
||||||
|
resolver.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func normalizedKeyFingerprint(_ keyFingerprint: String?) -> String? {
|
||||||
|
let normalized = keyFingerprint?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased()
|
||||||
|
return normalized?.isEmpty == false ? normalized : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func serviceIdentity(_ service: NWBrowser.Result) -> String {
|
||||||
|
String(describing: service.endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -606,6 +939,78 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
outgoingTransfers.removeValue(forKey: connection.id)
|
outgoingTransfers.removeValue(forKey: connection.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func receiverAuthenticationPending(
|
||||||
|
connection _: OutboundNearbyConnection,
|
||||||
|
certificateData: Data,
|
||||||
|
device: RemoteDeviceInfo,
|
||||||
|
fingerprint: String
|
||||||
|
) {
|
||||||
|
guard let normalizedFingerprint = normalizedKeyFingerprint(fingerprint) else { return }
|
||||||
|
log("[NearbyConnectionManager] Receiver auth pending: fingerprint=\(normalizedFingerprint) device=\(device.name ?? "unknown") (\(device.id ?? "no-id"))")
|
||||||
|
pendingNotificationSyncTrustQueue.sync {
|
||||||
|
pendingNotificationSyncTrust[normalizedFingerprint] = PendingReceiverTrust(
|
||||||
|
certificateData: certificateData,
|
||||||
|
device: device
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func commitNotificationSyncReceiverTrust(_ fingerprint: String) -> Bool {
|
||||||
|
guard let normalizedFingerprint = normalizedKeyFingerprint(fingerprint) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let pending = pendingNotificationSyncTrustQueue.sync {
|
||||||
|
pendingNotificationSyncTrust.removeValue(forKey: normalizedFingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending == nil {
|
||||||
|
let alreadyTrusted = TrustStore.shared.trustedCertificates.keys.contains(normalizedFingerprint)
|
||||||
|
if alreadyTrusted {
|
||||||
|
log("[NearbyConnectionManager] Commit trust skipped: already trusted for \(normalizedFingerprint)")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[NearbyConnectionManager] Commit trust failed: no pending entry for \(normalizedFingerprint)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let pending,
|
||||||
|
let certificate = try? Sharing_Nearby_PublicCertificate(serializedBytes: pending.certificateData) else {
|
||||||
|
log("[NearbyConnectionManager] Commit trust failed: could not parse certificate for \(normalizedFingerprint)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let derivedFingerprint = certificate.secretID.hex.lowercased()
|
||||||
|
guard derivedFingerprint == normalizedFingerprint else {
|
||||||
|
log("[NearbyConnectionManager] Commit trust failed: fingerprint mismatch pending=\(normalizedFingerprint) derived=\(derivedFingerprint)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[NearbyConnectionManager] Commit trust succeeded for fingerprint=\(normalizedFingerprint)")
|
||||||
|
TrustStore.shared.addTrusted(certificate: certificate, device: pending.device)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func clearPendingNotificationSyncReceiverTrust(_ fingerprint: String? = nil) {
|
||||||
|
if fingerprint == nil {
|
||||||
|
pendingNotificationSyncTrustQueue.sync {
|
||||||
|
pendingNotificationSyncTrust.removeAll()
|
||||||
|
}
|
||||||
|
log("[NearbyConnectionManager] Cleared all pending receiver trust entries")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let normalizedFingerprint = normalizedKeyFingerprint(fingerprint) else { return }
|
||||||
|
_ = pendingNotificationSyncTrustQueue.sync {
|
||||||
|
pendingNotificationSyncTrust.removeValue(forKey: normalizedFingerprint)
|
||||||
|
}
|
||||||
|
log("[NearbyConnectionManager] Cleared pending receiver trust for \(normalizedFingerprint)")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public func getActiveConnectionsCount() -> Int {
|
public func getActiveConnectionsCount() -> Int {
|
||||||
@@ -671,7 +1076,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
return securityScopeUrl
|
return securityScopeUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
if let bookmarkData = Settings.shared.saveFolderBookmark {
|
if let bookmarkData = Settings.sharedInstance.saveFolderBookmark {
|
||||||
var isStale = false
|
var isStale = false
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -721,6 +1126,14 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Callbacks
|
||||||
|
|
||||||
|
func informAboutStatus() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.connectionUpdateCallback(self.isConnectedToLocalNetwork && self.startedAdvertising)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// -- MARK: - Internal Data Model
|
// -- MARK: - Internal Data Model
|
||||||
|
|
||||||
@@ -736,4 +1149,10 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
let connection: OutboundNearbyConnection
|
let connection: OutboundNearbyConnection
|
||||||
let delegate: OutboundAppDelegate
|
let delegate: OutboundAppDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private struct PendingReceiverTrust {
|
||||||
|
let certificateData: Data
|
||||||
|
let device: RemoteDeviceInfo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//
|
||||||
|
// NearbyDeviceNameResolver.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 25.04.2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Darwin
|
||||||
|
import Foundation
|
||||||
|
import Network
|
||||||
|
|
||||||
|
final class NearbyDeviceNameResolver {
|
||||||
|
private static let hostnameLookupQueue = DispatchQueue(
|
||||||
|
label: "com.leonboettger.quickdrop.deviceNameHostnameLookup",
|
||||||
|
qos: .utility
|
||||||
|
)
|
||||||
|
|
||||||
|
private var hostnameResolvers: [UUID: NearbyBonjourServiceResolver] = [:]
|
||||||
|
private var lookupTokens: [String: UUID] = [:]
|
||||||
|
|
||||||
|
func needsLookup(for endpointInfo: EndpointInfo?) -> Bool {
|
||||||
|
let cleanedName = endpointInfo?.name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return cleanedName?.isEmpty != false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func resolveName(for service: NWBrowser.Result, endpointID: String, completion: @escaping (String) -> Void) {
|
||||||
|
let lookupToken = UUID()
|
||||||
|
lookupTokens[endpointID] = lookupToken
|
||||||
|
|
||||||
|
resolveAdvertisedHostname(for: service) { [weak self] hostname in
|
||||||
|
guard let self else { return }
|
||||||
|
guard self.lookupTokens[endpointID] == lookupToken else { return }
|
||||||
|
self.lookupTokens.removeValue(forKey: endpointID)
|
||||||
|
guard let hostname else { return }
|
||||||
|
completion(hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func cancelLookup(for endpointID: String) {
|
||||||
|
lookupTokens.removeValue(forKey: endpointID)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func cancelAll() {
|
||||||
|
lookupTokens.removeAll()
|
||||||
|
hostnameResolvers.values.forEach { $0.cancel() }
|
||||||
|
hostnameResolvers.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func resolveAdvertisedHostname(for service: NWBrowser.Result, completion: @escaping (String?) -> Void) {
|
||||||
|
let resolverID = UUID()
|
||||||
|
guard let resolver = NearbyBonjourServiceResolver(
|
||||||
|
result: service,
|
||||||
|
resolveTimeout: 0.5,
|
||||||
|
watchdogGracePeriod: 1.0,
|
||||||
|
completion: { [weak self] endpoints in
|
||||||
|
self?.hostnameResolvers.removeValue(forKey: resolverID)
|
||||||
|
guard let ipv4Address = endpoints.first(where: \.isIPv4)?.host else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Self.hostnameLookupQueue.async {
|
||||||
|
let hostname = NearbyHostLookup.hostname(forIPv4: ipv4Address)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hostnameResolvers[resolverID] = resolver
|
||||||
|
resolver.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private enum NearbyHostLookup {
|
||||||
|
static func hostname(forIPv4 address: String) -> String? {
|
||||||
|
reverseDNSName(forIPv4: address).flatMap(prettifyDisplayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func reverseDNSName(forIPv4 address: String) -> String? {
|
||||||
|
var socketAddress = sockaddr_in()
|
||||||
|
socketAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
||||||
|
socketAddress.sin_family = sa_family_t(AF_INET)
|
||||||
|
|
||||||
|
guard inet_pton(AF_INET, address, &socketAddress.sin_addr) == 1 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||||
|
let result = withUnsafePointer(to: &socketAddress) { pointer in
|
||||||
|
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPointer in
|
||||||
|
getnameinfo(
|
||||||
|
sockaddrPointer,
|
||||||
|
socklen_t(MemoryLayout<sockaddr_in>.size),
|
||||||
|
&hostBuffer,
|
||||||
|
socklen_t(hostBuffer.count),
|
||||||
|
nil,
|
||||||
|
0,
|
||||||
|
NI_NAMEREQD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard result == 0 else { return nil }
|
||||||
|
return String(cString: hostBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func prettifyDisplayName(_ value: String?) -> String? {
|
||||||
|
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let withoutZone = value.split(separator: "%").first.map(String.init) ?? value
|
||||||
|
let withoutDomain = withoutZone.split(separator: ".").first.map(String.init) ?? withoutZone
|
||||||
|
let prettified = withoutDomain
|
||||||
|
.replacingOccurrences(of: "-", with: " ")
|
||||||
|
.replacingOccurrences(of: "_", with: " ")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
guard !prettified.isEmpty else { return nil }
|
||||||
|
return prettified.capitalized
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,11 +22,14 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
private var currentState: State = .initial
|
private var currentState: State = .initial
|
||||||
private let urlsToSend: [URL]
|
private let urlsToSend: [URL]
|
||||||
private let textToSend: String?
|
private let textToSend: String?
|
||||||
|
private let isClipboardSyncTransfer: Bool
|
||||||
|
private let receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
|
||||||
private var ukeyClientFinishMsgData: Data?
|
private var ukeyClientFinishMsgData: Data?
|
||||||
private var queue: [OutgoingFileTransfer] = []
|
private var queue: [OutgoingFileTransfer] = []
|
||||||
private var currentTransfer: OutgoingFileTransfer?
|
private var currentTransfer: OutgoingFileTransfer?
|
||||||
private var totalBytesToSend: Int64 = 0
|
private var totalBytesToSend: Int64 = 0
|
||||||
private var textPayloadID: Int64 = 0
|
private var textPayloadID: Int64 = 0
|
||||||
|
private var receiverAuthenticationVerified = false
|
||||||
|
|
||||||
public var qrCodePrivateKey: ECPrivateKey?
|
public var qrCodePrivateKey: ECPrivateKey?
|
||||||
public var delegate: OutboundNearbyConnectionDelegate?
|
public var delegate: OutboundNearbyConnectionDelegate?
|
||||||
@@ -35,10 +38,19 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
case initial, sentUkeyClientInit, sentUkeyClientFinish, sentPairedKeyEncryption, sentPairedKeyResult, sentIntroduction, sendingFiles
|
case initial, sentUkeyClientInit, sentUkeyClientFinish, sentPairedKeyEncryption, sentPairedKeyResult, sentIntroduction, sendingFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
init(connection: NWConnection, id: String, urlsToSend: [URL], textToSend: String?) {
|
init(
|
||||||
|
connection: NWConnection,
|
||||||
|
id: String,
|
||||||
|
urlsToSend: [URL],
|
||||||
|
textToSend: String?,
|
||||||
|
isClipboardSyncTransfer: Bool = false,
|
||||||
|
receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
|
||||||
|
) {
|
||||||
|
|
||||||
self.urlsToSend = urlsToSend
|
self.urlsToSend = urlsToSend
|
||||||
self.textToSend = textToSend
|
self.textToSend = textToSend
|
||||||
|
self.isClipboardSyncTransfer = isClipboardSyncTransfer
|
||||||
|
self.receiverAuthenticationPolicy = receiverAuthenticationPolicy
|
||||||
|
|
||||||
super.init(connection: connection, id: id)
|
super.init(connection: connection, id: id)
|
||||||
|
|
||||||
@@ -147,15 +159,50 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case .sentPairedKeyEncryption:
|
case .sentPairedKeyEncryption:
|
||||||
try processPairedKeyEncryption(frame: frame)
|
if frame.hasV1, frame.v1.hasPairedKeyEncryption {
|
||||||
|
try processPairedKeyEncryption(frame: frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasPairedKeyResult {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Received pairedKeyResult before pairedKeyEncryption; continuing for legacy compatibility.")
|
||||||
|
try processPairedKeyResult(frame: frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .progressUpdate || frame.v1.type == .response {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for pairedKeyEncryption.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyEncryption")
|
||||||
case .sentPairedKeyResult:
|
case .sentPairedKeyResult:
|
||||||
try processPairedKeyResult(frame: frame)
|
if frame.hasV1, frame.v1.hasPairedKeyResult {
|
||||||
|
try processPairedKeyResult(frame: frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .pairedKeyEncryption || frame.v1.type == .progressUpdate || frame.v1.type == .response {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for pairedKeyResult.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyResult")
|
||||||
case .sentIntroduction:
|
case .sentIntroduction:
|
||||||
try processConsent(frame: frame)
|
if frame.version == .v1, frame.v1.type == .response {
|
||||||
|
try processConsent(frame: frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .pairedKeyEncryption || frame.v1.type == .pairedKeyResult || frame.v1.type == .progressUpdate {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for consent.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.type==response")
|
||||||
case .sendingFiles:
|
case .sendingFiles:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
assertionFailure("[OutboundNearbyConnection \(self.id)] Unexpected state \(currentState)")
|
log("[OutboundNearbyConnection \(self.id)] Unexpected state \(currentState)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +316,12 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
private func processConnectionResponse(frame: Location_Nearby_Connections_OfflineFrame) throws {
|
private func processConnectionResponse(frame: Location_Nearby_Connections_OfflineFrame) throws {
|
||||||
|
|
||||||
guard frame.version == .v1 else { throw NearbyError.protocolError("Unexpected offline frame version \(frame.version)") }
|
guard frame.version == .v1 else { throw NearbyError.protocolError("Unexpected offline frame version \(frame.version)") }
|
||||||
|
guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.type") }
|
||||||
|
|
||||||
|
if frame.v1.type == .keepAlive {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Ignoring keep-alive while waiting for connection response")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
guard frame.v1.type == .connectionResponse else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
|
guard frame.v1.type == .connectionResponse else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
|
||||||
|
|
||||||
@@ -327,7 +380,12 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
pairedResult.v1 = Sharing_Nearby_V1Frame()
|
pairedResult.v1 = Sharing_Nearby_V1Frame()
|
||||||
pairedResult.v1.type = .pairedKeyResult
|
pairedResult.v1.type = .pairedKeyResult
|
||||||
pairedResult.v1.pairedKeyResult = Sharing_Nearby_PairedKeyResultFrame()
|
pairedResult.v1.pairedKeyResult = Sharing_Nearby_PairedKeyResultFrame()
|
||||||
pairedResult.v1.pairedKeyResult.status = .unable
|
if receiverAuthenticationPolicy.requiresReceiverAuthentication {
|
||||||
|
try verifyReceiverAuthentication(frame: frame)
|
||||||
|
pairedResult.v1.pairedKeyResult.status = .success
|
||||||
|
} else {
|
||||||
|
pairedResult.v1.pairedKeyResult.status = .unable
|
||||||
|
}
|
||||||
|
|
||||||
try sendTransferSetupFrame(pairedResult)
|
try sendTransferSetupFrame(pairedResult)
|
||||||
currentState = .sentPairedKeyResult
|
currentState = .sentPairedKeyResult
|
||||||
@@ -336,10 +394,16 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
private func processPairedKeyResult(frame: Sharing_Nearby_Frame) throws {
|
private func processPairedKeyResult(frame: Sharing_Nearby_Frame) throws {
|
||||||
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyResult") }
|
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyResult") }
|
||||||
|
if receiverAuthenticationPolicy.requiresReceiverAuthentication, !receiverAuthenticationVerified {
|
||||||
|
throw NearbyError.protocolError("Receiver authentication incomplete")
|
||||||
|
}
|
||||||
|
|
||||||
var introduction = Sharing_Nearby_Frame()
|
var introduction = Sharing_Nearby_Frame()
|
||||||
introduction.version = .v1
|
introduction.version = .v1
|
||||||
introduction.v1.type = .introduction
|
introduction.v1.type = .introduction
|
||||||
|
if isClipboardSyncTransfer {
|
||||||
|
introduction.v1.introduction.useCase = .remoteCopy
|
||||||
|
}
|
||||||
|
|
||||||
if let textToSend = textToSend {
|
if let textToSend = textToSend {
|
||||||
var meta = Sharing_Nearby_TextMetadata()
|
var meta = Sharing_Nearby_TextMetadata()
|
||||||
@@ -430,8 +494,86 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
try sendDisconnectionAndDisconnect()
|
try sendDisconnectionAndDisconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func verifyReceiverAuthentication(frame: Sharing_Nearby_Frame) throws {
|
||||||
|
guard let expectedFingerprint = receiverAuthenticationPolicy.expectedReceiverKeyFingerprint, !expectedFingerprint.isEmpty else {
|
||||||
|
throw NearbyError.protocolError("Missing expected receiver fingerprint")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let certificate = frame.v1.certificateInfo.publicCertificate.first else {
|
||||||
|
throw NearbyError.protocolError("Missing receiver certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
let certificateData = try certificate.serializedData()
|
||||||
|
let secretID = certificate.secretID
|
||||||
|
guard !secretID.isEmpty else {
|
||||||
|
throw NearbyError.protocolError("Missing receiver key ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
let peerGenericKey = try Securemessage_GenericPublicKey(serializedBytes: certificate.publicKey)
|
||||||
|
guard let derivedKeyID = peerGenericKey.id() else {
|
||||||
|
throw NearbyError.protocolError("Receiver key ID derivation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard secretID == derivedKeyID else {
|
||||||
|
throw NearbyError.protocolError("Receiver key ID mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
let actualFingerprint = derivedKeyID.hex.lowercased()
|
||||||
|
guard actualFingerprint == expectedFingerprint else {
|
||||||
|
throw NearbyError.protocolError("Receiver fingerprint mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
let pinnedCertificateData = TrustStore.shared.findTrustedKey(for: secretID)
|
||||||
|
if pinnedCertificateData == nil, !receiverAuthenticationPolicy.allowsReceiverTrustBootstrap {
|
||||||
|
throw NearbyError.protocolError("Receiver certificate is not trusted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let pinnedCertificateData, pinnedCertificateData != certificateData {
|
||||||
|
throw NearbyError.protocolError("Receiver certificate changed")
|
||||||
|
}
|
||||||
|
|
||||||
|
let pairedKeyEncryption = frame.v1.pairedKeyEncryption
|
||||||
|
guard pairedKeyEncryption.secretIDHash == secretID else {
|
||||||
|
throw NearbyError.protocolError("Receiver sent unexpected key ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let authKeyData = authKey?.data() else {
|
||||||
|
throw NearbyError.protocolError("Missing auth key for receiver verification")
|
||||||
|
}
|
||||||
|
|
||||||
|
let domain = Domain.instance(curve: .EC256r1)
|
||||||
|
let ecKey = peerGenericKey.ecP256PublicKey
|
||||||
|
let point = Point(
|
||||||
|
BInt(magnitude: [UInt8](ecKey.x.ensureLength(length: 32))),
|
||||||
|
BInt(magnitude: [UInt8](ecKey.y.ensureLength(length: 32)))
|
||||||
|
)
|
||||||
|
|
||||||
|
guard let peerPublicKey = try? ECPublicKey(domain: domain, w: point) else {
|
||||||
|
throw NearbyError.protocolError("Invalid receiver public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature = try ECSignature(asn1: .build(pairedKeyEncryption.signedData), domain: domain)
|
||||||
|
guard peerPublicKey.verify(signature: signature, msg: authKeyData) else {
|
||||||
|
throw NearbyError.protocolError("Receiver signature verification failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pinnedCertificateData == nil, receiverAuthenticationPolicy.allowsReceiverTrustBootstrap {
|
||||||
|
guard let device = remoteDeviceInfo else {
|
||||||
|
throw NearbyError.protocolError("Missing remote device info for receiver trust")
|
||||||
|
}
|
||||||
|
delegate?.receiverAuthenticationPending(
|
||||||
|
connection: self,
|
||||||
|
certificateData: certificateData,
|
||||||
|
device: device,
|
||||||
|
fingerprint: actualFingerprint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
receiverAuthenticationVerified = true
|
||||||
|
}
|
||||||
|
|
||||||
private func hasURL() -> Bool {
|
private func hasURL() -> Bool {
|
||||||
urlsToSend.count == 1 && !urlsToSend[0].isFileURL
|
urlsToSend.count == 1 && !urlsToSend[0].isFileURL
|
||||||
}
|
}
|
||||||
@@ -552,3 +694,32 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
var currentOffset: Int64
|
var currentOffset: Int64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension ReceiverAuthenticationPolicy {
|
||||||
|
var expectedReceiverKeyFingerprint: String? {
|
||||||
|
switch self {
|
||||||
|
case .none:
|
||||||
|
return nil
|
||||||
|
case .trustedReceiver(let fingerprint), .bootstrapReceiverTrust(let fingerprint):
|
||||||
|
return Self.normalizedFingerprint(fingerprint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowsReceiverTrustBootstrap: Bool {
|
||||||
|
if case .bootstrapReceiverTrust = self {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiresReceiverAuthentication: Bool {
|
||||||
|
if case .none = self {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static func normalizedFingerprint(_ fingerprint: String) -> String {
|
||||||
|
fingerprint.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import UniformTypeIdentifiers
|
||||||
import Network
|
import Network
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import SwiftECC
|
import SwiftECC
|
||||||
@@ -18,6 +19,7 @@ protocol InboundNearbyConnectionDelegate {
|
|||||||
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double)
|
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double)
|
||||||
func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?)
|
func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?)
|
||||||
func showPlusScreen()
|
func showPlusScreen()
|
||||||
|
func pairingSetupConfirmed(connection: InboundNearbyConnection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -27,15 +29,33 @@ protocol OutboundNearbyConnectionDelegate {
|
|||||||
func transferAccepted(connection: OutboundNearbyConnection)
|
func transferAccepted(connection: OutboundNearbyConnection)
|
||||||
func failedWithError(connection: OutboundNearbyConnection, error: Error)
|
func failedWithError(connection: OutboundNearbyConnection, error: Error)
|
||||||
func transferFinished(connection: OutboundNearbyConnection)
|
func transferFinished(connection: OutboundNearbyConnection)
|
||||||
|
func receiverAuthenticationPending(
|
||||||
|
connection: OutboundNearbyConnection,
|
||||||
|
certificateData: Data,
|
||||||
|
device: RemoteDeviceInfo,
|
||||||
|
fingerprint: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public enum ReceiverAuthenticationPolicy: Equatable {
|
||||||
|
case none
|
||||||
|
case trustedReceiver(fingerprint: String)
|
||||||
|
case bootstrapReceiverTrust(fingerprint: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public protocol InboundAppDelegate: AnyObject {
|
public protocol InboundAppDelegate: AnyObject {
|
||||||
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
||||||
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
||||||
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, error: Error?)
|
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, wasClipboardSync: Bool, error: Error?)
|
||||||
func transferProgress(connectionID: String, progress: Double)
|
func transferProgress(connectionID: String, progress: Double)
|
||||||
func showPlusScreen()
|
func showPlusScreen()
|
||||||
|
func pairingSetupConfirmed()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InboundAppDelegate {
|
||||||
|
func pairingSetupConfirmed() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -55,20 +75,87 @@ public struct RemoteDeviceInfo: Codable, Identifiable, Equatable {
|
|||||||
public let name: String?
|
public let name: String?
|
||||||
public let type: DeviceType
|
public let type: DeviceType
|
||||||
public let qrCodeData: Data?
|
public let qrCodeData: Data?
|
||||||
|
public let isQuickDropPeer: Bool
|
||||||
public var id: String?
|
public var id: String?
|
||||||
|
public let keyFingerprint: String?
|
||||||
|
public let supportsNotificationSync: Bool
|
||||||
|
public let supportsClipboardReceive: Bool
|
||||||
|
|
||||||
init(name: String?, type: DeviceType?, id: String? = nil) {
|
init(
|
||||||
|
name: String?,
|
||||||
|
type: DeviceType?,
|
||||||
|
id: String? = nil,
|
||||||
|
isQuickDropPeer: Bool = false,
|
||||||
|
keyFingerprint: String? = nil,
|
||||||
|
supportsNotificationSync: Bool = false,
|
||||||
|
supportsClipboardReceive: Bool = false
|
||||||
|
) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.type = type ?? .phone
|
self.type = type ?? .phone
|
||||||
self.id = id
|
self.id = id
|
||||||
self.qrCodeData = nil
|
self.qrCodeData = nil
|
||||||
|
self.isQuickDropPeer = isQuickDropPeer
|
||||||
|
self.keyFingerprint = Self.normalizedKeyFingerprint(keyFingerprint)
|
||||||
|
self.supportsNotificationSync = supportsNotificationSync
|
||||||
|
self.supportsClipboardReceive = supportsClipboardReceive
|
||||||
}
|
}
|
||||||
|
|
||||||
init(info: EndpointInfo, id: String? = nil) {
|
init(
|
||||||
|
info: EndpointInfo,
|
||||||
|
id: String? = nil,
|
||||||
|
isQuickDropPeer: Bool = false,
|
||||||
|
keyFingerprint: String? = nil,
|
||||||
|
supportsNotificationSync: Bool = false,
|
||||||
|
supportsClipboardReceive: Bool = false
|
||||||
|
) {
|
||||||
self.name = info.name
|
self.name = info.name
|
||||||
self.type = info.deviceType
|
self.type = info.deviceType
|
||||||
self.qrCodeData = info.qrCodeData
|
self.qrCodeData = info.qrCodeData
|
||||||
self.id = id
|
self.id = id
|
||||||
|
self.isQuickDropPeer = isQuickDropPeer
|
||||||
|
self.keyFingerprint = Self.normalizedKeyFingerprint(keyFingerprint)
|
||||||
|
self.supportsNotificationSync = supportsNotificationSync
|
||||||
|
self.supportsClipboardReceive = supportsClipboardReceive
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case name
|
||||||
|
case type
|
||||||
|
case qrCodeData
|
||||||
|
case isQuickDropPeer
|
||||||
|
case isVerifiedQuickDropPeer
|
||||||
|
case id
|
||||||
|
case keyFingerprint
|
||||||
|
case supportsNotificationSync
|
||||||
|
case supportsClipboardReceive
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
name = try container.decodeIfPresent(String.self, forKey: .name)
|
||||||
|
type = try container.decode(DeviceType.self, forKey: .type)
|
||||||
|
qrCodeData = try container.decodeIfPresent(Data.self, forKey: .qrCodeData)
|
||||||
|
isQuickDropPeer = try container.decodeIfPresent(Bool.self, forKey: .isQuickDropPeer)
|
||||||
|
?? container.decodeIfPresent(Bool.self, forKey: .isVerifiedQuickDropPeer)
|
||||||
|
?? false
|
||||||
|
id = try container.decodeIfPresent(String.self, forKey: .id)
|
||||||
|
keyFingerprint = Self.normalizedKeyFingerprint(
|
||||||
|
try container.decodeIfPresent(String.self, forKey: .keyFingerprint)
|
||||||
|
)
|
||||||
|
supportsNotificationSync = try container.decodeIfPresent(Bool.self, forKey: .supportsNotificationSync) ?? false
|
||||||
|
supportsClipboardReceive = try container.decodeIfPresent(Bool.self, forKey: .supportsClipboardReceive) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encodeIfPresent(name, forKey: .name)
|
||||||
|
try container.encode(type, forKey: .type)
|
||||||
|
try container.encodeIfPresent(qrCodeData, forKey: .qrCodeData)
|
||||||
|
try container.encode(isQuickDropPeer, forKey: .isQuickDropPeer)
|
||||||
|
try container.encodeIfPresent(id, forKey: .id)
|
||||||
|
try container.encodeIfPresent(keyFingerprint, forKey: .keyFingerprint)
|
||||||
|
try container.encode(supportsNotificationSync, forKey: .supportsNotificationSync)
|
||||||
|
try container.encode(supportsClipboardReceive, forKey: .supportsClipboardReceive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -113,6 +200,13 @@ public struct RemoteDeviceInfo: Codable, Identifiable, Equatable {
|
|||||||
}
|
}
|
||||||
return "iphone"
|
return "iphone"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func normalizedKeyFingerprint(_ keyFingerprint: String?) -> String? {
|
||||||
|
let normalized = keyFingerprint?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased()
|
||||||
|
return normalized?.isEmpty == false ? normalized : nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -123,45 +217,94 @@ public struct TransferMetadata {
|
|||||||
public let textDescription: String?
|
public let textDescription: String?
|
||||||
public let type: TransferType
|
public let type: TransferType
|
||||||
public let allowsToBeAddedAsTrustedDevice: Bool
|
public let allowsToBeAddedAsTrustedDevice: Bool
|
||||||
|
public let isClipboardSync: Bool
|
||||||
|
|
||||||
init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file, allowsToBeAddedAsTrustedDevice: Bool) {
|
init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file, allowsToBeAddedAsTrustedDevice: Bool, isClipboardSync: Bool = false) {
|
||||||
self.files = files
|
self.files = files
|
||||||
self.id = id
|
self.id = id
|
||||||
self.pinCode = pinCode
|
self.pinCode = pinCode
|
||||||
self.textDescription = textDescription
|
self.textDescription = textDescription
|
||||||
self.type = transferType
|
self.type = transferType
|
||||||
self.allowsToBeAddedAsTrustedDevice = allowsToBeAddedAsTrustedDevice
|
self.allowsToBeAddedAsTrustedDevice = allowsToBeAddedAsTrustedDevice
|
||||||
|
self.isClipboardSync = isClipboardSync
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum TransferType {
|
public enum TransferType {
|
||||||
case file
|
case file
|
||||||
case text
|
case text
|
||||||
case url
|
case url
|
||||||
|
case wifiPassword
|
||||||
|
case notificationSync
|
||||||
|
case clipboardSync
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getSummary() -> String {
|
private func getSummary() -> String {
|
||||||
if let textTitle = textDescription {
|
if let textTitle = textDescription {
|
||||||
return textTitle
|
return textTitle
|
||||||
} else if files.count == 1 {
|
} else if files.count == 1 {
|
||||||
return files[0].name
|
return fileTypeSummary(for: files[0])
|
||||||
} else {
|
} else {
|
||||||
return String.localizedStringWithFormat("NFiles".localized(), files.count)
|
return String.localizedStringWithFormat("NFiles".localized(), files.count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func fileTypeSummary(for file: FileMetadata) -> String {
|
||||||
|
let mimeType = file.mimeType.lowercased()
|
||||||
|
let ext = URL(fileURLWithPath: file.name).pathExtension.lowercased()
|
||||||
|
let type = UTType(mimeType: mimeType) ?? UTType(filenameExtension: ext)
|
||||||
|
|
||||||
|
if let type, type.conforms(to: .pdf) {
|
||||||
|
return UTType.pdf.localizedDescription ?? "PDF"
|
||||||
|
}
|
||||||
|
if let type, type.conforms(to: .image) {
|
||||||
|
return UTType.image.localizedDescription ?? "Image"
|
||||||
|
}
|
||||||
|
if let type, type.conforms(to: .video) || type.conforms(to: .movie) {
|
||||||
|
return UTType.video.localizedDescription ?? "Video"
|
||||||
|
}
|
||||||
|
if let type, type.conforms(to: .audio) {
|
||||||
|
return UTType.audio.localizedDescription ?? "Audio"
|
||||||
|
}
|
||||||
|
if let type, type.conforms(to: .text) {
|
||||||
|
return UTType.text.localizedDescription ?? "Text"
|
||||||
|
}
|
||||||
|
if let type, type.conforms(to: .archive) {
|
||||||
|
return UTType.archive.localizedDescription ?? "Archive"
|
||||||
|
}
|
||||||
|
if let type, let description = type.localizedDescription {
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
return "File"
|
||||||
|
}
|
||||||
|
|
||||||
public func getDescription(deviceName: String, alreadyAccepted: Bool) -> String {
|
public func getDescription(deviceName: String) -> String {
|
||||||
|
|
||||||
let fileStr = getSummary()
|
let fileStr = getSummary()
|
||||||
|
|
||||||
switch type {
|
switch type {
|
||||||
case .file:
|
case .file:
|
||||||
return String(format: (alreadyAccepted ? "DeviceCurrentlySendingFiles" : "DeviceSendingFiles").localized(), arguments: [deviceName, fileStr])
|
return "DeviceSendingFiles".localized(with: fileStr, deviceName)
|
||||||
|
|
||||||
case .text:
|
case .text:
|
||||||
return String(format: (alreadyAccepted ? "DeviceCurrentlySendingText" : "DeviceSendingText").localized(), arguments: [deviceName, fileStr])
|
return "DeviceSendingText".localized(with: deviceName)
|
||||||
|
|
||||||
case .url:
|
case .url:
|
||||||
return String(format: (alreadyAccepted ? "DeviceCurrentlySendingUrl" : "DeviceSendingUrl").localized(), arguments: [deviceName, fileStr])
|
return "DeviceSendingUrl".localized(with: deviceName)
|
||||||
|
|
||||||
|
case .wifiPassword:
|
||||||
|
return "DeviceSendingWifiPassword".localized(with: deviceName)
|
||||||
|
|
||||||
|
case .notificationSync:
|
||||||
|
if let textDescription, !textDescription.isEmpty {
|
||||||
|
return textDescription
|
||||||
|
}
|
||||||
|
return "NotificationSyncConsentPromptFromDevice".localized(with: deviceName)
|
||||||
|
|
||||||
|
case .clipboardSync:
|
||||||
|
if let textDescription, !textDescription.isEmpty {
|
||||||
|
return textDescription
|
||||||
|
}
|
||||||
|
return "ClipboardSyncConsentPromptFromDevice".localized(with: deviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +313,31 @@ public struct TransferMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PairingUseCase: String, Codable, Hashable {
|
||||||
|
case notificationSync = "notification_sync"
|
||||||
|
case clipboardSync = "clipboard_sync"
|
||||||
|
|
||||||
|
var protoValue: Sharing_Nearby_PairingMetadata.UseCase {
|
||||||
|
switch self {
|
||||||
|
case .notificationSync:
|
||||||
|
return .notificationSync
|
||||||
|
case .clipboardSync:
|
||||||
|
return .clipboardSync
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(protoValue: Sharing_Nearby_PairingMetadata.UseCase) {
|
||||||
|
switch protoValue {
|
||||||
|
case .notificationSync:
|
||||||
|
self = .notificationSync
|
||||||
|
case .clipboardSync:
|
||||||
|
self = .clipboardSync
|
||||||
|
case .unknown:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct FileMetadata {
|
public struct FileMetadata {
|
||||||
public let name: String
|
public let name: String
|
||||||
@@ -289,6 +457,7 @@ public enum NearbyError: Error {
|
|||||||
case packetFilterError
|
case packetFilterError
|
||||||
case firewallError
|
case firewallError
|
||||||
case inputOutput
|
case inputOutput
|
||||||
|
case notificationSyncNotTrusted
|
||||||
case canceled(reason: CancellationReason)
|
case canceled(reason: CancellationReason)
|
||||||
|
|
||||||
public enum CancellationReason {
|
public enum CancellationReason {
|
||||||
|
|||||||
@@ -334,6 +334,151 @@ struct Sharing_Nearby_WifiCredentialsMetadata: Sendable {
|
|||||||
fileprivate var _id: Int64? = nil
|
fileprivate var _id: Int64? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// NEXT_ID=10
|
||||||
|
struct Sharing_Nearby_MirroredNotificationMetadata: Sendable {
|
||||||
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
|
// methods supported on all messages.
|
||||||
|
|
||||||
|
var version: Int32 {
|
||||||
|
get {return _version ?? 1}
|
||||||
|
set {_version = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `version` has been explicitly set.
|
||||||
|
var hasVersion: Bool {return self._version != nil}
|
||||||
|
/// Clears the value of `version`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearVersion() {self._version = nil}
|
||||||
|
|
||||||
|
var notificationKey: String {
|
||||||
|
get {return _notificationKey ?? String()}
|
||||||
|
set {_notificationKey = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `notificationKey` has been explicitly set.
|
||||||
|
var hasNotificationKey: Bool {return self._notificationKey != nil}
|
||||||
|
/// Clears the value of `notificationKey`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearNotificationKey() {self._notificationKey = nil}
|
||||||
|
|
||||||
|
var packageName: String {
|
||||||
|
get {return _packageName ?? String()}
|
||||||
|
set {_packageName = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `packageName` has been explicitly set.
|
||||||
|
var hasPackageName: Bool {return self._packageName != nil}
|
||||||
|
/// Clears the value of `packageName`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearPackageName() {self._packageName = nil}
|
||||||
|
|
||||||
|
var appName: String {
|
||||||
|
get {return _appName ?? String()}
|
||||||
|
set {_appName = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `appName` has been explicitly set.
|
||||||
|
var hasAppName: Bool {return self._appName != nil}
|
||||||
|
/// Clears the value of `appName`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearAppName() {self._appName = nil}
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
get {return _title ?? String()}
|
||||||
|
set {_title = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `title` has been explicitly set.
|
||||||
|
var hasTitle: Bool {return self._title != nil}
|
||||||
|
/// Clears the value of `title`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearTitle() {self._title = nil}
|
||||||
|
|
||||||
|
var body: String {
|
||||||
|
get {return _body ?? String()}
|
||||||
|
set {_body = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `body` has been explicitly set.
|
||||||
|
var hasBody: Bool {return self._body != nil}
|
||||||
|
/// Clears the value of `body`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearBody() {self._body = nil}
|
||||||
|
|
||||||
|
var postedAtEpochMs: Int64 {
|
||||||
|
get {return _postedAtEpochMs ?? 0}
|
||||||
|
set {_postedAtEpochMs = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `postedAtEpochMs` has been explicitly set.
|
||||||
|
var hasPostedAtEpochMs: Bool {return self._postedAtEpochMs != nil}
|
||||||
|
/// Clears the value of `postedAtEpochMs`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearPostedAtEpochMs() {self._postedAtEpochMs = nil}
|
||||||
|
|
||||||
|
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
fileprivate var _version: Int32? = nil
|
||||||
|
fileprivate var _notificationKey: String? = nil
|
||||||
|
fileprivate var _packageName: String? = nil
|
||||||
|
fileprivate var _appName: String? = nil
|
||||||
|
fileprivate var _title: String? = nil
|
||||||
|
fileprivate var _body: String? = nil
|
||||||
|
fileprivate var _postedAtEpochMs: Int64? = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NEXT_ID=5
|
||||||
|
struct Sharing_Nearby_PairingMetadata: Sendable {
|
||||||
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
|
// methods supported on all messages.
|
||||||
|
|
||||||
|
var version: Int32 {
|
||||||
|
get {return _version ?? 1}
|
||||||
|
set {_version = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `version` has been explicitly set.
|
||||||
|
var hasVersion: Bool {return self._version != nil}
|
||||||
|
/// Clears the value of `version`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearVersion() {self._version = nil}
|
||||||
|
|
||||||
|
var useCase: Sharing_Nearby_PairingMetadata.UseCase {
|
||||||
|
get {return _useCase ?? .unknown}
|
||||||
|
set {_useCase = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `useCase` has been explicitly set.
|
||||||
|
var hasUseCase: Bool {return self._useCase != nil}
|
||||||
|
/// Clears the value of `useCase`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearUseCase() {self._useCase = nil}
|
||||||
|
|
||||||
|
var setupRequest: Bool {
|
||||||
|
get {return _setupRequest ?? false}
|
||||||
|
set {_setupRequest = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `setupRequest` has been explicitly set.
|
||||||
|
var hasSetupRequest: Bool {return self._setupRequest != nil}
|
||||||
|
/// Clears the value of `setupRequest`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearSetupRequest() {self._setupRequest = nil}
|
||||||
|
|
||||||
|
var setupPinHash: Data {
|
||||||
|
get {return _setupPinHash ?? Data()}
|
||||||
|
set {_setupPinHash = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `setupPinHash` has been explicitly set.
|
||||||
|
var hasSetupPinHash: Bool {return self._setupPinHash != nil}
|
||||||
|
/// Clears the value of `setupPinHash`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearSetupPinHash() {self._setupPinHash = nil}
|
||||||
|
|
||||||
|
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||||
|
|
||||||
|
enum UseCase: Int, SwiftProtobuf.Enum, Swift.CaseIterable {
|
||||||
|
case unknown = 0
|
||||||
|
case notificationSync = 1
|
||||||
|
case clipboardSync = 2
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self = .unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
fileprivate var _version: Int32? = nil
|
||||||
|
fileprivate var _useCase: Sharing_Nearby_PairingMetadata.UseCase? = nil
|
||||||
|
fileprivate var _setupRequest: Bool? = nil
|
||||||
|
fileprivate var _setupPinHash: Data? = nil
|
||||||
|
}
|
||||||
|
|
||||||
/// NEXT_ID=8
|
/// NEXT_ID=8
|
||||||
struct Sharing_Nearby_AppMetadata: Sendable {
|
struct Sharing_Nearby_AppMetadata: Sendable {
|
||||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
@@ -505,76 +650,76 @@ struct Sharing_Nearby_Frame: Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// NEXT_ID=8
|
/// NEXT_ID=8
|
||||||
struct Sharing_Nearby_V1Frame: @unchecked Sendable {
|
struct Sharing_Nearby_V1Frame: Sendable {
|
||||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
// methods supported on all messages.
|
// methods supported on all messages.
|
||||||
|
|
||||||
var type: Sharing_Nearby_V1Frame.FrameType {
|
var type: Sharing_Nearby_V1Frame.FrameType {
|
||||||
get {return _storage._type ?? .unknownFrameType}
|
get {return _type ?? .unknownFrameType}
|
||||||
set {_uniqueStorage()._type = newValue}
|
set {_type = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `type` has been explicitly set.
|
/// Returns true if `type` has been explicitly set.
|
||||||
var hasType: Bool {return _storage._type != nil}
|
var hasType: Bool {return self._type != nil}
|
||||||
/// Clears the value of `type`. Subsequent reads from it will return its default value.
|
/// Clears the value of `type`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearType() {_uniqueStorage()._type = nil}
|
mutating func clearType() {self._type = nil}
|
||||||
|
|
||||||
/// At most one of the following fields will be set.
|
/// At most one of the following fields will be set.
|
||||||
var introduction: Sharing_Nearby_IntroductionFrame {
|
var introduction: Sharing_Nearby_IntroductionFrame {
|
||||||
get {return _storage._introduction ?? Sharing_Nearby_IntroductionFrame()}
|
get {return _introduction ?? Sharing_Nearby_IntroductionFrame()}
|
||||||
set {_uniqueStorage()._introduction = newValue}
|
set {_introduction = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `introduction` has been explicitly set.
|
/// Returns true if `introduction` has been explicitly set.
|
||||||
var hasIntroduction: Bool {return _storage._introduction != nil}
|
var hasIntroduction: Bool {return self._introduction != nil}
|
||||||
/// Clears the value of `introduction`. Subsequent reads from it will return its default value.
|
/// Clears the value of `introduction`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearIntroduction() {_uniqueStorage()._introduction = nil}
|
mutating func clearIntroduction() {self._introduction = nil}
|
||||||
|
|
||||||
var connectionResponse: Sharing_Nearby_ConnectionResponseFrame {
|
var connectionResponse: Sharing_Nearby_ConnectionResponseFrame {
|
||||||
get {return _storage._connectionResponse ?? Sharing_Nearby_ConnectionResponseFrame()}
|
get {return _connectionResponse ?? Sharing_Nearby_ConnectionResponseFrame()}
|
||||||
set {_uniqueStorage()._connectionResponse = newValue}
|
set {_connectionResponse = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `connectionResponse` has been explicitly set.
|
/// Returns true if `connectionResponse` has been explicitly set.
|
||||||
var hasConnectionResponse: Bool {return _storage._connectionResponse != nil}
|
var hasConnectionResponse: Bool {return self._connectionResponse != nil}
|
||||||
/// Clears the value of `connectionResponse`. Subsequent reads from it will return its default value.
|
/// Clears the value of `connectionResponse`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearConnectionResponse() {_uniqueStorage()._connectionResponse = nil}
|
mutating func clearConnectionResponse() {self._connectionResponse = nil}
|
||||||
|
|
||||||
var pairedKeyEncryption: Sharing_Nearby_PairedKeyEncryptionFrame {
|
var pairedKeyEncryption: Sharing_Nearby_PairedKeyEncryptionFrame {
|
||||||
get {return _storage._pairedKeyEncryption ?? Sharing_Nearby_PairedKeyEncryptionFrame()}
|
get {return _pairedKeyEncryption ?? Sharing_Nearby_PairedKeyEncryptionFrame()}
|
||||||
set {_uniqueStorage()._pairedKeyEncryption = newValue}
|
set {_pairedKeyEncryption = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `pairedKeyEncryption` has been explicitly set.
|
/// Returns true if `pairedKeyEncryption` has been explicitly set.
|
||||||
var hasPairedKeyEncryption: Bool {return _storage._pairedKeyEncryption != nil}
|
var hasPairedKeyEncryption: Bool {return self._pairedKeyEncryption != nil}
|
||||||
/// Clears the value of `pairedKeyEncryption`. Subsequent reads from it will return its default value.
|
/// Clears the value of `pairedKeyEncryption`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearPairedKeyEncryption() {_uniqueStorage()._pairedKeyEncryption = nil}
|
mutating func clearPairedKeyEncryption() {self._pairedKeyEncryption = nil}
|
||||||
|
|
||||||
var pairedKeyResult: Sharing_Nearby_PairedKeyResultFrame {
|
var pairedKeyResult: Sharing_Nearby_PairedKeyResultFrame {
|
||||||
get {return _storage._pairedKeyResult ?? Sharing_Nearby_PairedKeyResultFrame()}
|
get {return _pairedKeyResult ?? Sharing_Nearby_PairedKeyResultFrame()}
|
||||||
set {_uniqueStorage()._pairedKeyResult = newValue}
|
set {_pairedKeyResult = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `pairedKeyResult` has been explicitly set.
|
/// Returns true if `pairedKeyResult` has been explicitly set.
|
||||||
var hasPairedKeyResult: Bool {return _storage._pairedKeyResult != nil}
|
var hasPairedKeyResult: Bool {return self._pairedKeyResult != nil}
|
||||||
/// Clears the value of `pairedKeyResult`. Subsequent reads from it will return its default value.
|
/// Clears the value of `pairedKeyResult`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearPairedKeyResult() {_uniqueStorage()._pairedKeyResult = nil}
|
mutating func clearPairedKeyResult() {self._pairedKeyResult = nil}
|
||||||
|
|
||||||
/// NOTE: This field was marked as deprecated in the .proto file.
|
/// NOTE: This field was marked as deprecated in the .proto file.
|
||||||
var certificateInfo: Sharing_Nearby_CertificateInfoFrame {
|
var certificateInfo: Sharing_Nearby_CertificateInfoFrame {
|
||||||
get {return _storage._certificateInfo ?? Sharing_Nearby_CertificateInfoFrame()}
|
get {return _certificateInfo ?? Sharing_Nearby_CertificateInfoFrame()}
|
||||||
set {_uniqueStorage()._certificateInfo = newValue}
|
set {_certificateInfo = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `certificateInfo` has been explicitly set.
|
/// Returns true if `certificateInfo` has been explicitly set.
|
||||||
var hasCertificateInfo: Bool {return _storage._certificateInfo != nil}
|
var hasCertificateInfo: Bool {return self._certificateInfo != nil}
|
||||||
/// Clears the value of `certificateInfo`. Subsequent reads from it will return its default value.
|
/// Clears the value of `certificateInfo`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearCertificateInfo() {_uniqueStorage()._certificateInfo = nil}
|
mutating func clearCertificateInfo() {self._certificateInfo = nil}
|
||||||
|
|
||||||
/// NOTE: This field was marked as deprecated in the .proto file.
|
/// NOTE: This field was marked as deprecated in the .proto file.
|
||||||
var progressUpdate: Sharing_Nearby_ProgressUpdateFrame {
|
var progressUpdate: Sharing_Nearby_ProgressUpdateFrame {
|
||||||
get {return _storage._progressUpdate ?? Sharing_Nearby_ProgressUpdateFrame()}
|
get {return _progressUpdate ?? Sharing_Nearby_ProgressUpdateFrame()}
|
||||||
set {_uniqueStorage()._progressUpdate = newValue}
|
set {_progressUpdate = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `progressUpdate` has been explicitly set.
|
/// Returns true if `progressUpdate` has been explicitly set.
|
||||||
var hasProgressUpdate: Bool {return _storage._progressUpdate != nil}
|
var hasProgressUpdate: Bool {return self._progressUpdate != nil}
|
||||||
/// Clears the value of `progressUpdate`. Subsequent reads from it will return its default value.
|
/// Clears the value of `progressUpdate`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearProgressUpdate() {_uniqueStorage()._progressUpdate = nil}
|
mutating func clearProgressUpdate() {self._progressUpdate = nil}
|
||||||
|
|
||||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||||
|
|
||||||
@@ -600,56 +745,98 @@ struct Sharing_Nearby_V1Frame: @unchecked Sendable {
|
|||||||
|
|
||||||
init() {}
|
init() {}
|
||||||
|
|
||||||
fileprivate var _storage = _StorageClass.defaultInstance
|
fileprivate var _type: Sharing_Nearby_V1Frame.FrameType? = nil
|
||||||
|
fileprivate var _introduction: Sharing_Nearby_IntroductionFrame? = nil
|
||||||
|
fileprivate var _connectionResponse: Sharing_Nearby_ConnectionResponseFrame? = nil
|
||||||
|
fileprivate var _pairedKeyEncryption: Sharing_Nearby_PairedKeyEncryptionFrame? = nil
|
||||||
|
fileprivate var _pairedKeyResult: Sharing_Nearby_PairedKeyResultFrame? = nil
|
||||||
|
fileprivate var _certificateInfo: Sharing_Nearby_CertificateInfoFrame? = nil
|
||||||
|
fileprivate var _progressUpdate: Sharing_Nearby_ProgressUpdateFrame? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An introduction packet sent by the sending side. Contains a list of files
|
/// An introduction packet sent by the sending side. Contains a list of files
|
||||||
/// they'd like to share.
|
/// they'd like to share.
|
||||||
/// NEXT_ID=10
|
/// NEXT_ID=1003
|
||||||
struct Sharing_Nearby_IntroductionFrame: Sendable {
|
struct Sharing_Nearby_IntroductionFrame: @unchecked Sendable {
|
||||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
// methods supported on all messages.
|
// methods supported on all messages.
|
||||||
|
|
||||||
var fileMetadata: [Sharing_Nearby_FileMetadata] = []
|
var fileMetadata: [Sharing_Nearby_FileMetadata] {
|
||||||
|
get {return _storage._fileMetadata}
|
||||||
|
set {_uniqueStorage()._fileMetadata = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
var textMetadata: [Sharing_Nearby_TextMetadata] = []
|
var textMetadata: [Sharing_Nearby_TextMetadata] {
|
||||||
|
get {return _storage._textMetadata}
|
||||||
|
set {_uniqueStorage()._textMetadata = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
/// The required app package to open the content. May be null.
|
/// The required app package to open the content. May be null.
|
||||||
var requiredPackage: String {
|
var requiredPackage: String {
|
||||||
get {return _requiredPackage ?? String()}
|
get {return _storage._requiredPackage ?? String()}
|
||||||
set {_requiredPackage = newValue}
|
set {_uniqueStorage()._requiredPackage = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `requiredPackage` has been explicitly set.
|
/// Returns true if `requiredPackage` has been explicitly set.
|
||||||
var hasRequiredPackage: Bool {return self._requiredPackage != nil}
|
var hasRequiredPackage: Bool {return _storage._requiredPackage != nil}
|
||||||
/// Clears the value of `requiredPackage`. Subsequent reads from it will return its default value.
|
/// Clears the value of `requiredPackage`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearRequiredPackage() {self._requiredPackage = nil}
|
mutating func clearRequiredPackage() {_uniqueStorage()._requiredPackage = nil}
|
||||||
|
|
||||||
var wifiCredentialsMetadata: [Sharing_Nearby_WifiCredentialsMetadata] = []
|
var wifiCredentialsMetadata: [Sharing_Nearby_WifiCredentialsMetadata] {
|
||||||
|
get {return _storage._wifiCredentialsMetadata}
|
||||||
|
set {_uniqueStorage()._wifiCredentialsMetadata = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
var appMetadata: [Sharing_Nearby_AppMetadata] = []
|
var appMetadata: [Sharing_Nearby_AppMetadata] {
|
||||||
|
get {return _storage._appMetadata}
|
||||||
|
set {_uniqueStorage()._appMetadata = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
var startTransfer: Bool {
|
var startTransfer: Bool {
|
||||||
get {return _startTransfer ?? false}
|
get {return _storage._startTransfer ?? false}
|
||||||
set {_startTransfer = newValue}
|
set {_uniqueStorage()._startTransfer = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `startTransfer` has been explicitly set.
|
/// Returns true if `startTransfer` has been explicitly set.
|
||||||
var hasStartTransfer: Bool {return self._startTransfer != nil}
|
var hasStartTransfer: Bool {return _storage._startTransfer != nil}
|
||||||
/// Clears the value of `startTransfer`. Subsequent reads from it will return its default value.
|
/// Clears the value of `startTransfer`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearStartTransfer() {self._startTransfer = nil}
|
mutating func clearStartTransfer() {_uniqueStorage()._startTransfer = nil}
|
||||||
|
|
||||||
var streamMetadata: [Sharing_Nearby_StreamMetadata] = []
|
var streamMetadata: [Sharing_Nearby_StreamMetadata] {
|
||||||
|
get {return _storage._streamMetadata}
|
||||||
|
set {_uniqueStorage()._streamMetadata = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
var useCase: Sharing_Nearby_IntroductionFrame.SharingUseCase {
|
var useCase: Sharing_Nearby_IntroductionFrame.SharingUseCase {
|
||||||
get {return _useCase ?? .unknown}
|
get {return _storage._useCase ?? .unknown}
|
||||||
set {_useCase = newValue}
|
set {_uniqueStorage()._useCase = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `useCase` has been explicitly set.
|
/// Returns true if `useCase` has been explicitly set.
|
||||||
var hasUseCase: Bool {return self._useCase != nil}
|
var hasUseCase: Bool {return _storage._useCase != nil}
|
||||||
/// Clears the value of `useCase`. Subsequent reads from it will return its default value.
|
/// Clears the value of `useCase`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearUseCase() {self._useCase = nil}
|
mutating func clearUseCase() {_uniqueStorage()._useCase = nil}
|
||||||
|
|
||||||
var previewPayloadIds: [Int64] = []
|
var previewPayloadIds: [Int64] {
|
||||||
|
get {return _storage._previewPayloadIds}
|
||||||
|
set {_uniqueStorage()._previewPayloadIds = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata {
|
||||||
|
get {return _storage._mirroredNotificationMetadata ?? Sharing_Nearby_MirroredNotificationMetadata()}
|
||||||
|
set {_uniqueStorage()._mirroredNotificationMetadata = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `mirroredNotificationMetadata` has been explicitly set.
|
||||||
|
var hasMirroredNotificationMetadata: Bool {return _storage._mirroredNotificationMetadata != nil}
|
||||||
|
/// Clears the value of `mirroredNotificationMetadata`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearMirroredNotificationMetadata() {_uniqueStorage()._mirroredNotificationMetadata = nil}
|
||||||
|
|
||||||
|
var pairingMetadata: Sharing_Nearby_PairingMetadata {
|
||||||
|
get {return _storage._pairingMetadata ?? Sharing_Nearby_PairingMetadata()}
|
||||||
|
set {_uniqueStorage()._pairingMetadata = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `pairingMetadata` has been explicitly set.
|
||||||
|
var hasPairingMetadata: Bool {return _storage._pairingMetadata != nil}
|
||||||
|
/// Clears the value of `pairingMetadata`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearPairingMetadata() {_uniqueStorage()._pairingMetadata = nil}
|
||||||
|
|
||||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||||
|
|
||||||
@@ -666,9 +853,7 @@ struct Sharing_Nearby_IntroductionFrame: Sendable {
|
|||||||
|
|
||||||
init() {}
|
init() {}
|
||||||
|
|
||||||
fileprivate var _requiredPackage: String? = nil
|
fileprivate var _storage = _StorageClass.defaultInstance
|
||||||
fileprivate var _startTransfer: Bool? = nil
|
|
||||||
fileprivate var _useCase: Sharing_Nearby_IntroductionFrame.SharingUseCase? = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A progress update packet sent by the sending side. Contains transfer progress
|
/// A progress update packet sent by the sending side. Contains transfer progress
|
||||||
@@ -1371,6 +1556,123 @@ extension Sharing_Nearby_WifiCredentialsMetadata.SecurityType: SwiftProtobuf._Pr
|
|||||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_SECURITY_TYPE\0\u{1}OPEN\0\u{1}WPA_PSK\0\u{1}WEP\0\u{1}SAE\0")
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_SECURITY_TYPE\0\u{1}OPEN\0\u{1}WPA_PSK\0\u{1}WEP\0\u{1}SAE\0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Sharing_Nearby_MirroredNotificationMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let protoMessageName: String = _protobuf_package + ".MirroredNotificationMetadata"
|
||||||
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}version\0\u{3}notification_key\0\u{3}package_name\0\u{3}app_name\0\u{1}title\0\u{1}body\0\u{3}posted_at_epoch_ms\0\u{c}\u{8}\u{1}\u{c}\u{9}\u{1}")
|
||||||
|
|
||||||
|
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||||
|
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every case branch when no optimizations are
|
||||||
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
|
switch fieldNumber {
|
||||||
|
case 1: try { try decoder.decodeSingularInt32Field(value: &self._version) }()
|
||||||
|
case 2: try { try decoder.decodeSingularStringField(value: &self._notificationKey) }()
|
||||||
|
case 3: try { try decoder.decodeSingularStringField(value: &self._packageName) }()
|
||||||
|
case 4: try { try decoder.decodeSingularStringField(value: &self._appName) }()
|
||||||
|
case 5: try { try decoder.decodeSingularStringField(value: &self._title) }()
|
||||||
|
case 6: try { try decoder.decodeSingularStringField(value: &self._body) }()
|
||||||
|
case 7: try { try decoder.decodeSingularInt64Field(value: &self._postedAtEpochMs) }()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every if/case branch local when no optimizations
|
||||||
|
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||||
|
// https://github.com/apple/swift-protobuf/issues/1182
|
||||||
|
try { if let v = self._version {
|
||||||
|
try visitor.visitSingularInt32Field(value: v, fieldNumber: 1)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._notificationKey {
|
||||||
|
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._packageName {
|
||||||
|
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._appName {
|
||||||
|
try visitor.visitSingularStringField(value: v, fieldNumber: 4)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._title {
|
||||||
|
try visitor.visitSingularStringField(value: v, fieldNumber: 5)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._body {
|
||||||
|
try visitor.visitSingularStringField(value: v, fieldNumber: 6)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._postedAtEpochMs {
|
||||||
|
try visitor.visitSingularInt64Field(value: v, fieldNumber: 7)
|
||||||
|
} }()
|
||||||
|
try unknownFields.traverse(visitor: &visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: Sharing_Nearby_MirroredNotificationMetadata, rhs: Sharing_Nearby_MirroredNotificationMetadata) -> Bool {
|
||||||
|
if lhs._version != rhs._version {return false}
|
||||||
|
if lhs._notificationKey != rhs._notificationKey {return false}
|
||||||
|
if lhs._packageName != rhs._packageName {return false}
|
||||||
|
if lhs._appName != rhs._appName {return false}
|
||||||
|
if lhs._title != rhs._title {return false}
|
||||||
|
if lhs._body != rhs._body {return false}
|
||||||
|
if lhs._postedAtEpochMs != rhs._postedAtEpochMs {return false}
|
||||||
|
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sharing_Nearby_PairingMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let protoMessageName: String = _protobuf_package + ".PairingMetadata"
|
||||||
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}version\0\u{3}use_case\0\u{3}setup_request\0\u{3}setup_pin_hash\0")
|
||||||
|
|
||||||
|
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||||
|
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every case branch when no optimizations are
|
||||||
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
|
switch fieldNumber {
|
||||||
|
case 1: try { try decoder.decodeSingularInt32Field(value: &self._version) }()
|
||||||
|
case 2: try { try decoder.decodeSingularEnumField(value: &self._useCase) }()
|
||||||
|
case 3: try { try decoder.decodeSingularBoolField(value: &self._setupRequest) }()
|
||||||
|
case 4: try { try decoder.decodeSingularBytesField(value: &self._setupPinHash) }()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every if/case branch local when no optimizations
|
||||||
|
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||||
|
// https://github.com/apple/swift-protobuf/issues/1182
|
||||||
|
try { if let v = self._version {
|
||||||
|
try visitor.visitSingularInt32Field(value: v, fieldNumber: 1)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._useCase {
|
||||||
|
try visitor.visitSingularEnumField(value: v, fieldNumber: 2)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._setupRequest {
|
||||||
|
try visitor.visitSingularBoolField(value: v, fieldNumber: 3)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._setupPinHash {
|
||||||
|
try visitor.visitSingularBytesField(value: v, fieldNumber: 4)
|
||||||
|
} }()
|
||||||
|
try unknownFields.traverse(visitor: &visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: Sharing_Nearby_PairingMetadata, rhs: Sharing_Nearby_PairingMetadata) -> Bool {
|
||||||
|
if lhs._version != rhs._version {return false}
|
||||||
|
if lhs._useCase != rhs._useCase {return false}
|
||||||
|
if lhs._setupRequest != rhs._setupRequest {return false}
|
||||||
|
if lhs._setupPinHash != rhs._setupPinHash {return false}
|
||||||
|
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sharing_Nearby_PairingMetadata.UseCase: SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN\0\u{1}NOTIFICATION_SYNC\0\u{1}CLIPBOARD_SYNC\0")
|
||||||
|
}
|
||||||
|
|
||||||
extension Sharing_Nearby_AppMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
extension Sharing_Nearby_AppMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||||
static let protoMessageName: String = _protobuf_package + ".AppMetadata"
|
static let protoMessageName: String = _protobuf_package + ".AppMetadata"
|
||||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}app_name\0\u{1}size\0\u{3}payload_id\0\u{1}id\0\u{3}file_name\0\u{3}file_size\0\u{3}package_name\0")
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}app_name\0\u{1}size\0\u{3}payload_id\0\u{1}id\0\u{3}file_name\0\u{3}file_size\0\u{3}package_name\0")
|
||||||
@@ -1531,14 +1833,86 @@ extension Sharing_Nearby_V1Frame: SwiftProtobuf.Message, SwiftProtobuf._MessageI
|
|||||||
static let protoMessageName: String = _protobuf_package + ".V1Frame"
|
static let protoMessageName: String = _protobuf_package + ".V1Frame"
|
||||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}type\0\u{1}introduction\0\u{3}connection_response\0\u{3}paired_key_encryption\0\u{3}paired_key_result\0\u{3}certificate_info\0\u{3}progress_update\0")
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}type\0\u{1}introduction\0\u{3}connection_response\0\u{3}paired_key_encryption\0\u{3}paired_key_result\0\u{3}certificate_info\0\u{3}progress_update\0")
|
||||||
|
|
||||||
|
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||||
|
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every case branch when no optimizations are
|
||||||
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
|
switch fieldNumber {
|
||||||
|
case 1: try { try decoder.decodeSingularEnumField(value: &self._type) }()
|
||||||
|
case 2: try { try decoder.decodeSingularMessageField(value: &self._introduction) }()
|
||||||
|
case 3: try { try decoder.decodeSingularMessageField(value: &self._connectionResponse) }()
|
||||||
|
case 4: try { try decoder.decodeSingularMessageField(value: &self._pairedKeyEncryption) }()
|
||||||
|
case 5: try { try decoder.decodeSingularMessageField(value: &self._pairedKeyResult) }()
|
||||||
|
case 6: try { try decoder.decodeSingularMessageField(value: &self._certificateInfo) }()
|
||||||
|
case 7: try { try decoder.decodeSingularMessageField(value: &self._progressUpdate) }()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every if/case branch local when no optimizations
|
||||||
|
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||||
|
// https://github.com/apple/swift-protobuf/issues/1182
|
||||||
|
try { if let v = self._type {
|
||||||
|
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._introduction {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._connectionResponse {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._pairedKeyEncryption {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._pairedKeyResult {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._certificateInfo {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._progressUpdate {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
|
||||||
|
} }()
|
||||||
|
try unknownFields.traverse(visitor: &visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: Sharing_Nearby_V1Frame, rhs: Sharing_Nearby_V1Frame) -> Bool {
|
||||||
|
if lhs._type != rhs._type {return false}
|
||||||
|
if lhs._introduction != rhs._introduction {return false}
|
||||||
|
if lhs._connectionResponse != rhs._connectionResponse {return false}
|
||||||
|
if lhs._pairedKeyEncryption != rhs._pairedKeyEncryption {return false}
|
||||||
|
if lhs._pairedKeyResult != rhs._pairedKeyResult {return false}
|
||||||
|
if lhs._certificateInfo != rhs._certificateInfo {return false}
|
||||||
|
if lhs._progressUpdate != rhs._progressUpdate {return false}
|
||||||
|
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sharing_Nearby_V1Frame.FrameType: SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_FRAME_TYPE\0\u{1}INTRODUCTION\0\u{1}RESPONSE\0\u{1}PAIRED_KEY_ENCRYPTION\0\u{1}PAIRED_KEY_RESULT\0\u{1}CERTIFICATE_INFO\0\u{1}CANCEL\0\u{1}PROGRESS_UPDATE\0")
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sharing_Nearby_IntroductionFrame: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let protoMessageName: String = _protobuf_package + ".IntroductionFrame"
|
||||||
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}file_metadata\0\u{3}text_metadata\0\u{3}required_package\0\u{3}wifi_credentials_metadata\0\u{3}app_metadata\0\u{3}start_transfer\0\u{3}stream_metadata\0\u{3}use_case\0\u{3}preview_payload_ids\0\u{4}`\u{f}mirrored_notification_metadata\0\u{3}pairing_metadata\0")
|
||||||
|
|
||||||
fileprivate class _StorageClass {
|
fileprivate class _StorageClass {
|
||||||
var _type: Sharing_Nearby_V1Frame.FrameType? = nil
|
var _fileMetadata: [Sharing_Nearby_FileMetadata] = []
|
||||||
var _introduction: Sharing_Nearby_IntroductionFrame? = nil
|
var _textMetadata: [Sharing_Nearby_TextMetadata] = []
|
||||||
var _connectionResponse: Sharing_Nearby_ConnectionResponseFrame? = nil
|
var _requiredPackage: String? = nil
|
||||||
var _pairedKeyEncryption: Sharing_Nearby_PairedKeyEncryptionFrame? = nil
|
var _wifiCredentialsMetadata: [Sharing_Nearby_WifiCredentialsMetadata] = []
|
||||||
var _pairedKeyResult: Sharing_Nearby_PairedKeyResultFrame? = nil
|
var _appMetadata: [Sharing_Nearby_AppMetadata] = []
|
||||||
var _certificateInfo: Sharing_Nearby_CertificateInfoFrame? = nil
|
var _startTransfer: Bool? = nil
|
||||||
var _progressUpdate: Sharing_Nearby_ProgressUpdateFrame? = nil
|
var _streamMetadata: [Sharing_Nearby_StreamMetadata] = []
|
||||||
|
var _useCase: Sharing_Nearby_IntroductionFrame.SharingUseCase? = nil
|
||||||
|
var _previewPayloadIds: [Int64] = []
|
||||||
|
var _mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata? = nil
|
||||||
|
var _pairingMetadata: Sharing_Nearby_PairingMetadata? = nil
|
||||||
|
|
||||||
// This property is used as the initial default value for new instances of the type.
|
// This property is used as the initial default value for new instances of the type.
|
||||||
// The type itself is protecting the reference to its storage via CoW semantics.
|
// The type itself is protecting the reference to its storage via CoW semantics.
|
||||||
@@ -1549,13 +1923,17 @@ extension Sharing_Nearby_V1Frame: SwiftProtobuf.Message, SwiftProtobuf._MessageI
|
|||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
init(copying source: _StorageClass) {
|
init(copying source: _StorageClass) {
|
||||||
_type = source._type
|
_fileMetadata = source._fileMetadata
|
||||||
_introduction = source._introduction
|
_textMetadata = source._textMetadata
|
||||||
_connectionResponse = source._connectionResponse
|
_requiredPackage = source._requiredPackage
|
||||||
_pairedKeyEncryption = source._pairedKeyEncryption
|
_wifiCredentialsMetadata = source._wifiCredentialsMetadata
|
||||||
_pairedKeyResult = source._pairedKeyResult
|
_appMetadata = source._appMetadata
|
||||||
_certificateInfo = source._certificateInfo
|
_startTransfer = source._startTransfer
|
||||||
_progressUpdate = source._progressUpdate
|
_streamMetadata = source._streamMetadata
|
||||||
|
_useCase = source._useCase
|
||||||
|
_previewPayloadIds = source._previewPayloadIds
|
||||||
|
_mirroredNotificationMetadata = source._mirroredNotificationMetadata
|
||||||
|
_pairingMetadata = source._pairingMetadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1574,13 +1952,17 @@ extension Sharing_Nearby_V1Frame: SwiftProtobuf.Message, SwiftProtobuf._MessageI
|
|||||||
// allocates stack space for every case branch when no optimizations are
|
// allocates stack space for every case branch when no optimizations are
|
||||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
switch fieldNumber {
|
switch fieldNumber {
|
||||||
case 1: try { try decoder.decodeSingularEnumField(value: &_storage._type) }()
|
case 1: try { try decoder.decodeRepeatedMessageField(value: &_storage._fileMetadata) }()
|
||||||
case 2: try { try decoder.decodeSingularMessageField(value: &_storage._introduction) }()
|
case 2: try { try decoder.decodeRepeatedMessageField(value: &_storage._textMetadata) }()
|
||||||
case 3: try { try decoder.decodeSingularMessageField(value: &_storage._connectionResponse) }()
|
case 3: try { try decoder.decodeSingularStringField(value: &_storage._requiredPackage) }()
|
||||||
case 4: try { try decoder.decodeSingularMessageField(value: &_storage._pairedKeyEncryption) }()
|
case 4: try { try decoder.decodeRepeatedMessageField(value: &_storage._wifiCredentialsMetadata) }()
|
||||||
case 5: try { try decoder.decodeSingularMessageField(value: &_storage._pairedKeyResult) }()
|
case 5: try { try decoder.decodeRepeatedMessageField(value: &_storage._appMetadata) }()
|
||||||
case 6: try { try decoder.decodeSingularMessageField(value: &_storage._certificateInfo) }()
|
case 6: try { try decoder.decodeSingularBoolField(value: &_storage._startTransfer) }()
|
||||||
case 7: try { try decoder.decodeSingularMessageField(value: &_storage._progressUpdate) }()
|
case 7: try { try decoder.decodeRepeatedMessageField(value: &_storage._streamMetadata) }()
|
||||||
|
case 8: try { try decoder.decodeSingularEnumField(value: &_storage._useCase) }()
|
||||||
|
case 9: try { try decoder.decodeRepeatedInt64Field(value: &_storage._previewPayloadIds) }()
|
||||||
|
case 1001: try { try decoder.decodeSingularMessageField(value: &_storage._mirroredNotificationMetadata) }()
|
||||||
|
case 1002: try { try decoder.decodeSingularMessageField(value: &_storage._pairingMetadata) }()
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1593,125 +1975,63 @@ extension Sharing_Nearby_V1Frame: SwiftProtobuf.Message, SwiftProtobuf._MessageI
|
|||||||
// allocates stack space for every if/case branch local when no optimizations
|
// allocates stack space for every if/case branch local when no optimizations
|
||||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||||
// https://github.com/apple/swift-protobuf/issues/1182
|
// https://github.com/apple/swift-protobuf/issues/1182
|
||||||
try { if let v = _storage._type {
|
if !_storage._fileMetadata.isEmpty {
|
||||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
|
try visitor.visitRepeatedMessageField(value: _storage._fileMetadata, fieldNumber: 1)
|
||||||
} }()
|
|
||||||
try { if let v = _storage._introduction {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
|
|
||||||
} }()
|
|
||||||
try { if let v = _storage._connectionResponse {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
|
|
||||||
} }()
|
|
||||||
try { if let v = _storage._pairedKeyEncryption {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
|
|
||||||
} }()
|
|
||||||
try { if let v = _storage._pairedKeyResult {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
|
|
||||||
} }()
|
|
||||||
try { if let v = _storage._certificateInfo {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
|
|
||||||
} }()
|
|
||||||
try { if let v = _storage._progressUpdate {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
|
|
||||||
} }()
|
|
||||||
}
|
|
||||||
try unknownFields.traverse(visitor: &visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func ==(lhs: Sharing_Nearby_V1Frame, rhs: Sharing_Nearby_V1Frame) -> Bool {
|
|
||||||
if lhs._storage !== rhs._storage {
|
|
||||||
let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
|
|
||||||
let _storage = _args.0
|
|
||||||
let rhs_storage = _args.1
|
|
||||||
if _storage._type != rhs_storage._type {return false}
|
|
||||||
if _storage._introduction != rhs_storage._introduction {return false}
|
|
||||||
if _storage._connectionResponse != rhs_storage._connectionResponse {return false}
|
|
||||||
if _storage._pairedKeyEncryption != rhs_storage._pairedKeyEncryption {return false}
|
|
||||||
if _storage._pairedKeyResult != rhs_storage._pairedKeyResult {return false}
|
|
||||||
if _storage._certificateInfo != rhs_storage._certificateInfo {return false}
|
|
||||||
if _storage._progressUpdate != rhs_storage._progressUpdate {return false}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
if !storagesAreEqual {return false}
|
if !_storage._textMetadata.isEmpty {
|
||||||
}
|
try visitor.visitRepeatedMessageField(value: _storage._textMetadata, fieldNumber: 2)
|
||||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Sharing_Nearby_V1Frame.FrameType: SwiftProtobuf._ProtoNameProviding {
|
|
||||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_FRAME_TYPE\0\u{1}INTRODUCTION\0\u{1}RESPONSE\0\u{1}PAIRED_KEY_ENCRYPTION\0\u{1}PAIRED_KEY_RESULT\0\u{1}CERTIFICATE_INFO\0\u{1}CANCEL\0\u{1}PROGRESS_UPDATE\0")
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Sharing_Nearby_IntroductionFrame: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
|
||||||
static let protoMessageName: String = _protobuf_package + ".IntroductionFrame"
|
|
||||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}file_metadata\0\u{3}text_metadata\0\u{3}required_package\0\u{3}wifi_credentials_metadata\0\u{3}app_metadata\0\u{3}start_transfer\0\u{3}stream_metadata\0\u{3}use_case\0\u{3}preview_payload_ids\0")
|
|
||||||
|
|
||||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
|
||||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
|
||||||
// The use of inline closures is to circumvent an issue where the compiler
|
|
||||||
// allocates stack space for every case branch when no optimizations are
|
|
||||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
|
||||||
switch fieldNumber {
|
|
||||||
case 1: try { try decoder.decodeRepeatedMessageField(value: &self.fileMetadata) }()
|
|
||||||
case 2: try { try decoder.decodeRepeatedMessageField(value: &self.textMetadata) }()
|
|
||||||
case 3: try { try decoder.decodeSingularStringField(value: &self._requiredPackage) }()
|
|
||||||
case 4: try { try decoder.decodeRepeatedMessageField(value: &self.wifiCredentialsMetadata) }()
|
|
||||||
case 5: try { try decoder.decodeRepeatedMessageField(value: &self.appMetadata) }()
|
|
||||||
case 6: try { try decoder.decodeSingularBoolField(value: &self._startTransfer) }()
|
|
||||||
case 7: try { try decoder.decodeRepeatedMessageField(value: &self.streamMetadata) }()
|
|
||||||
case 8: try { try decoder.decodeSingularEnumField(value: &self._useCase) }()
|
|
||||||
case 9: try { try decoder.decodeRepeatedInt64Field(value: &self.previewPayloadIds) }()
|
|
||||||
default: break
|
|
||||||
}
|
}
|
||||||
}
|
try { if let v = _storage._requiredPackage {
|
||||||
}
|
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||||
|
} }()
|
||||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
if !_storage._wifiCredentialsMetadata.isEmpty {
|
||||||
// The use of inline closures is to circumvent an issue where the compiler
|
try visitor.visitRepeatedMessageField(value: _storage._wifiCredentialsMetadata, fieldNumber: 4)
|
||||||
// allocates stack space for every if/case branch local when no optimizations
|
}
|
||||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
if !_storage._appMetadata.isEmpty {
|
||||||
// https://github.com/apple/swift-protobuf/issues/1182
|
try visitor.visitRepeatedMessageField(value: _storage._appMetadata, fieldNumber: 5)
|
||||||
if !self.fileMetadata.isEmpty {
|
}
|
||||||
try visitor.visitRepeatedMessageField(value: self.fileMetadata, fieldNumber: 1)
|
try { if let v = _storage._startTransfer {
|
||||||
}
|
try visitor.visitSingularBoolField(value: v, fieldNumber: 6)
|
||||||
if !self.textMetadata.isEmpty {
|
} }()
|
||||||
try visitor.visitRepeatedMessageField(value: self.textMetadata, fieldNumber: 2)
|
if !_storage._streamMetadata.isEmpty {
|
||||||
}
|
try visitor.visitRepeatedMessageField(value: _storage._streamMetadata, fieldNumber: 7)
|
||||||
try { if let v = self._requiredPackage {
|
}
|
||||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
try { if let v = _storage._useCase {
|
||||||
} }()
|
try visitor.visitSingularEnumField(value: v, fieldNumber: 8)
|
||||||
if !self.wifiCredentialsMetadata.isEmpty {
|
} }()
|
||||||
try visitor.visitRepeatedMessageField(value: self.wifiCredentialsMetadata, fieldNumber: 4)
|
if !_storage._previewPayloadIds.isEmpty {
|
||||||
}
|
try visitor.visitRepeatedInt64Field(value: _storage._previewPayloadIds, fieldNumber: 9)
|
||||||
if !self.appMetadata.isEmpty {
|
}
|
||||||
try visitor.visitRepeatedMessageField(value: self.appMetadata, fieldNumber: 5)
|
try { if let v = _storage._mirroredNotificationMetadata {
|
||||||
}
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 1001)
|
||||||
try { if let v = self._startTransfer {
|
} }()
|
||||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 6)
|
try { if let v = _storage._pairingMetadata {
|
||||||
} }()
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 1002)
|
||||||
if !self.streamMetadata.isEmpty {
|
} }()
|
||||||
try visitor.visitRepeatedMessageField(value: self.streamMetadata, fieldNumber: 7)
|
|
||||||
}
|
|
||||||
try { if let v = self._useCase {
|
|
||||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 8)
|
|
||||||
} }()
|
|
||||||
if !self.previewPayloadIds.isEmpty {
|
|
||||||
try visitor.visitRepeatedInt64Field(value: self.previewPayloadIds, fieldNumber: 9)
|
|
||||||
}
|
}
|
||||||
try unknownFields.traverse(visitor: &visitor)
|
try unknownFields.traverse(visitor: &visitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: Sharing_Nearby_IntroductionFrame, rhs: Sharing_Nearby_IntroductionFrame) -> Bool {
|
static func ==(lhs: Sharing_Nearby_IntroductionFrame, rhs: Sharing_Nearby_IntroductionFrame) -> Bool {
|
||||||
if lhs.fileMetadata != rhs.fileMetadata {return false}
|
if lhs._storage !== rhs._storage {
|
||||||
if lhs.textMetadata != rhs.textMetadata {return false}
|
let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
|
||||||
if lhs._requiredPackage != rhs._requiredPackage {return false}
|
let _storage = _args.0
|
||||||
if lhs.wifiCredentialsMetadata != rhs.wifiCredentialsMetadata {return false}
|
let rhs_storage = _args.1
|
||||||
if lhs.appMetadata != rhs.appMetadata {return false}
|
if _storage._fileMetadata != rhs_storage._fileMetadata {return false}
|
||||||
if lhs._startTransfer != rhs._startTransfer {return false}
|
if _storage._textMetadata != rhs_storage._textMetadata {return false}
|
||||||
if lhs.streamMetadata != rhs.streamMetadata {return false}
|
if _storage._requiredPackage != rhs_storage._requiredPackage {return false}
|
||||||
if lhs._useCase != rhs._useCase {return false}
|
if _storage._wifiCredentialsMetadata != rhs_storage._wifiCredentialsMetadata {return false}
|
||||||
if lhs.previewPayloadIds != rhs.previewPayloadIds {return false}
|
if _storage._appMetadata != rhs_storage._appMetadata {return false}
|
||||||
|
if _storage._startTransfer != rhs_storage._startTransfer {return false}
|
||||||
|
if _storage._streamMetadata != rhs_storage._streamMetadata {return false}
|
||||||
|
if _storage._useCase != rhs_storage._useCase {return false}
|
||||||
|
if _storage._previewPayloadIds != rhs_storage._previewPayloadIds {return false}
|
||||||
|
if _storage._mirroredNotificationMetadata != rhs_storage._mirroredNotificationMetadata {return false}
|
||||||
|
if _storage._pairingMetadata != rhs_storage._pairingMetadata {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !storagesAreEqual {return false}
|
||||||
|
}
|
||||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,32 @@ message WifiCredentialsMetadata {
|
|||||||
optional int64 id = 5;
|
optional int64 id = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEXT_ID=10
|
||||||
|
message MirroredNotificationMetadata {
|
||||||
|
optional int32 version = 1 [default = 1];
|
||||||
|
optional string notification_key = 2;
|
||||||
|
optional string package_name = 3;
|
||||||
|
optional string app_name = 4;
|
||||||
|
optional string title = 5;
|
||||||
|
optional string body = 6;
|
||||||
|
optional int64 posted_at_epoch_ms = 7;
|
||||||
|
reserved 8, 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEXT_ID=5
|
||||||
|
message PairingMetadata {
|
||||||
|
enum UseCase {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
NOTIFICATION_SYNC = 1;
|
||||||
|
CLIPBOARD_SYNC = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional int32 version = 1 [default = 1];
|
||||||
|
optional UseCase use_case = 2 [default = UNKNOWN];
|
||||||
|
optional bool setup_request = 3;
|
||||||
|
optional bytes setup_pin_hash = 4;
|
||||||
|
}
|
||||||
|
|
||||||
// NEXT_ID=8
|
// NEXT_ID=8
|
||||||
message AppMetadata {
|
message AppMetadata {
|
||||||
// The app name. This will be sent in introduction.
|
// The app name. This will be sent in introduction.
|
||||||
@@ -212,7 +238,7 @@ message V1Frame {
|
|||||||
|
|
||||||
// An introduction packet sent by the sending side. Contains a list of files
|
// An introduction packet sent by the sending side. Contains a list of files
|
||||||
// they'd like to share.
|
// they'd like to share.
|
||||||
// NEXT_ID=10
|
// NEXT_ID=1003
|
||||||
message IntroductionFrame {
|
message IntroductionFrame {
|
||||||
enum SharingUseCase {
|
enum SharingUseCase {
|
||||||
UNKNOWN = 0;
|
UNKNOWN = 0;
|
||||||
@@ -230,6 +256,8 @@ message IntroductionFrame {
|
|||||||
repeated StreamMetadata stream_metadata = 7;
|
repeated StreamMetadata stream_metadata = 7;
|
||||||
optional SharingUseCase use_case = 8;
|
optional SharingUseCase use_case = 8;
|
||||||
repeated int64 preview_payload_ids = 9;
|
repeated int64 preview_payload_ids = 9;
|
||||||
|
optional MirroredNotificationMetadata mirrored_notification_metadata = 1001;
|
||||||
|
optional PairingMetadata pairing_metadata = 1002;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A progress update packet sent by the sending side. Contains transfer progress
|
// A progress update packet sent by the sending side. Contains transfer progress
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Security
|
||||||
|
|
||||||
/// Contains information about the app group used to share data between main app and extensions
|
/// Contains information about the app group used to share data between main app and extensions
|
||||||
struct AppGroup {
|
struct AppGroup {
|
||||||
@@ -19,3 +20,90 @@ struct AppGroup {
|
|||||||
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct KeychainStore {
|
||||||
|
private static let service = "com.leonboettger.quickdrop.securestorage"
|
||||||
|
// Must match the shared keychain access group declared in all Apple entitlements.
|
||||||
|
private static let sharedAccessGroup = "92SDAWLN76.com.leonboettger.neardrop"
|
||||||
|
|
||||||
|
static func loadData(account: String) throws -> Data? {
|
||||||
|
var query = baseQuery(account: account)
|
||||||
|
query[kSecReturnData as String] = true
|
||||||
|
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
||||||
|
|
||||||
|
var result: CFTypeRef?
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case errSecSuccess:
|
||||||
|
return result as? Data
|
||||||
|
case errSecItemNotFound:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
throw KeychainStoreError.unexpectedStatus(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func saveData(_ data: Data, account: String) throws {
|
||||||
|
var addQuery = baseQuery(account: account)
|
||||||
|
addQuery[kSecValueData as String] = data
|
||||||
|
addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||||
|
|
||||||
|
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
||||||
|
if addStatus == errSecSuccess {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if addStatus != errSecDuplicateItem {
|
||||||
|
throw KeychainStoreError.unexpectedStatus(addStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributesToUpdate: [String: Any] = [
|
||||||
|
kSecValueData as String: data,
|
||||||
|
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||||
|
]
|
||||||
|
|
||||||
|
let updateStatus = SecItemUpdate(baseQuery(account: account) as CFDictionary, attributesToUpdate as CFDictionary)
|
||||||
|
guard updateStatus == errSecSuccess else {
|
||||||
|
throw KeychainStoreError.unexpectedStatus(updateStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func deleteItem(account: String) throws {
|
||||||
|
let status = SecItemDelete(baseQuery(account: account) as CFDictionary)
|
||||||
|
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||||
|
throw KeychainStoreError.unexpectedStatus(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func baseQuery(account: String) -> [String: Any] {
|
||||||
|
var query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: service,
|
||||||
|
kSecAttrAccount as String: account,
|
||||||
|
kSecUseDataProtectionKeychain as String: true,
|
||||||
|
]
|
||||||
|
|
||||||
|
query[kSecAttrAccessGroup as String] = sharedAccessGroup
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum KeychainStoreError: LocalizedError {
|
||||||
|
case unexpectedStatus(OSStatus)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case let .unexpectedStatus(status):
|
||||||
|
if let message = SecCopyErrorMessageString(status, nil) as String? {
|
||||||
|
return "Keychain error \(status): \(message)"
|
||||||
|
}
|
||||||
|
return "Keychain error \(status)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
//
|
|
||||||
// EXIFUtils.swift
|
|
||||||
// QuickDrop
|
|
||||||
//
|
|
||||||
// Created by Leon Böttger on 10.11.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import ImageIO
|
|
||||||
import LUI
|
|
||||||
|
|
||||||
struct EXIFUtils {
|
|
||||||
|
|
||||||
/// Applies EXIF timestamps to a given file or directory.
|
|
||||||
static func applyEXIFTimestamps(at url: URL) {
|
|
||||||
let fm = FileManager.default
|
|
||||||
var isDir: ObjCBool = false
|
|
||||||
guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return }
|
|
||||||
|
|
||||||
if isDir.boolValue {
|
|
||||||
// It's a directory — enumerate files
|
|
||||||
if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) {
|
|
||||||
for case let fileURL as URL in enumerator {
|
|
||||||
guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue }
|
|
||||||
applyEXIFTimestamp(to: fileURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// It's a single file
|
|
||||||
applyEXIFTimestamp(to: url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Applies EXIF timestamp (if available) to a single file
|
|
||||||
private static func applyEXIFTimestamp(to fileURL: URL) {
|
|
||||||
guard let exifDate = exifOriginalDate(from: fileURL) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
// Set the "creation date"
|
|
||||||
try FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
log("[EXIFUtils] Failed to set file attributes for \(fileURL.path): \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
|
|
||||||
static func exifOriginalDate(from url: URL) -> Date? {
|
|
||||||
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
|
|
||||||
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
|
|
||||||
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any],
|
|
||||||
let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String
|
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let offsetString = exif[kCGImagePropertyExifOffsetTimeOriginal] as? String
|
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
||||||
formatter.calendar = Calendar(identifier: .gregorian)
|
|
||||||
formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
|
|
||||||
|
|
||||||
let offsetSeconds = offsetString.flatMap(secondsFromOffsetString)
|
|
||||||
if let seconds = offsetSeconds {
|
|
||||||
formatter.timeZone = TimeZone(secondsFromGMT: seconds)
|
|
||||||
} else {
|
|
||||||
// Fallback: treat as device local
|
|
||||||
formatter.timeZone = TimeZone.current
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatter.date(from: dateString)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static private func secondsFromOffsetString(_ s: String) -> Int? {
|
|
||||||
// Expect formats like "+02:00" or "-05:30"
|
|
||||||
guard s.count == 6, (s.first == "+" || s.first == "-") else { return nil }
|
|
||||||
let sign = s.first == "-" ? -1 : 1
|
|
||||||
let parts = s.dropFirst().split(separator: ":")
|
|
||||||
guard parts.count == 2,
|
|
||||||
let hours = Int(parts[0]),
|
|
||||||
let minutes = Int(parts[1]) else { return nil }
|
|
||||||
return sign * (hours * 3600 + minutes * 60)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,9 @@
|
|||||||
// Created by Leon Böttger on 04.04.25.
|
// Created by Leon Böttger on 04.04.25.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import Foundation
|
||||||
|
import LUI
|
||||||
|
|
||||||
extension Data {
|
extension Data {
|
||||||
func urlSafeBase64EncodedString() -> String {
|
func urlSafeBase64EncodedString() -> String {
|
||||||
@@ -30,17 +31,6 @@ extension Data {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func suffixOfAtMost(numBytes: Int) -> Data {
|
|
||||||
|
|
||||||
if count <= numBytes {
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
return subdata(in: count-numBytes..<count)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static func dataFromUrlSafeBase64(_ str: String) -> Data? {
|
static func dataFromUrlSafeBase64(_ str: String) -> Data? {
|
||||||
var regularB64 = String(str.map {
|
var regularB64 = String(str.map {
|
||||||
if $0 == "_" {
|
if $0 == "_" {
|
||||||
@@ -111,10 +101,14 @@ extension String {
|
|||||||
|
|
||||||
|
|
||||||
public func fullVersion() -> Bool {
|
public func fullVersion() -> Bool {
|
||||||
return Settings.shared.gotPlus
|
#if GITHUB
|
||||||
|
return true
|
||||||
|
#else
|
||||||
|
return IAPManager.sharedInstance.plusVersionState
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func isFileTransferRestricted() -> Bool {
|
public func isFileTransferRestricted() -> Bool {
|
||||||
(!fullVersion() && Settings.shared.incomingTransmissionCount > 1)
|
(!fullVersion() && Settings.sharedInstance.incomingTransmissionCount > 1)
|
||||||
}
|
}
|
||||||
@@ -51,17 +51,21 @@ class IdentityManager {
|
|||||||
private func generateAndStoreKeyPair() -> (ECPrivateKey, ECPublicKey) {
|
private func generateAndStoreKeyPair() -> (ECPrivateKey, ECPublicKey) {
|
||||||
let domain = Domain.instance(curve: .EC256r1)
|
let domain = Domain.instance(curve: .EC256r1)
|
||||||
let (pubKey, privKey) = domain.makeKeyPair()
|
let (pubKey, privKey) = domain.makeKeyPair()
|
||||||
|
|
||||||
AppGroup.appGroupUD.set(Data(privKey.s.asSignedBytes()), forKey: signingKeyTag)
|
let privateKeyData = Data(privKey.s.asSignedBytes())
|
||||||
|
do {
|
||||||
|
try KeychainStore.saveData(privateKeyData, account: signingKeyTag)
|
||||||
|
AppGroup.appGroupUD.removeObject(forKey: signingKeyTag)
|
||||||
|
} catch {
|
||||||
|
log("[IdentityManager] Failed to store private key in keychain: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
return (privKey, pubKey)
|
return (privKey, pubKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func loadKeys() -> (ECPrivateKey, ECPublicKey)? {
|
private func loadKeys() -> (ECPrivateKey, ECPublicKey)? {
|
||||||
let keyData = AppGroup.appGroupUD.data(forKey: signingKeyTag)
|
guard let keyData = loadStoredKeyData() else { return nil }
|
||||||
|
|
||||||
guard let keyData = keyData else { return nil }
|
|
||||||
|
|
||||||
let domain = Domain.instance(curve: .EC256r1)
|
let domain = Domain.instance(curve: .EC256r1)
|
||||||
guard let privateKey = try? ECPrivateKey(domain: domain, s: .init(signed: Array(keyData))) else {
|
guard let privateKey = try? ECPrivateKey(domain: domain, s: .init(signed: Array(keyData))) else {
|
||||||
@@ -80,6 +84,32 @@ class IdentityManager {
|
|||||||
|
|
||||||
return (privateKey, publicKey)
|
return (privateKey, publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func loadStoredKeyData() -> Data? {
|
||||||
|
do {
|
||||||
|
if let keyData = try KeychainStore.loadData(account: signingKeyTag) {
|
||||||
|
AppGroup.appGroupUD.removeObject(forKey: signingKeyTag)
|
||||||
|
return keyData
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log("[IdentityManager] Failed to load private key from keychain: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let legacyKeyData = AppGroup.appGroupUD.data(forKey: signingKeyTag) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try KeychainStore.saveData(legacyKeyData, account: signingKeyTag)
|
||||||
|
AppGroup.appGroupUD.removeObject(forKey: signingKeyTag)
|
||||||
|
log("[IdentityManager] Migrated private key from shared defaults to keychain.")
|
||||||
|
} catch {
|
||||||
|
log("[IdentityManager] Failed to migrate private key to keychain: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return legacyKeyData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// PairingToken.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 2026-03-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import QRCode
|
||||||
|
import SwiftUI
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
enum PairingToken {
|
||||||
|
static func generate() -> String {
|
||||||
|
Data.randomData(length: 16).urlSafeBase64EncodedString()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func receiverFingerprintHex() -> String? {
|
||||||
|
guard let keyId = IdentityManager.shared.getPublicKey()?.toGenericPublicKey().id() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return keyId.hex.lowercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func qrPayload(token: String, receiverFingerprint: String, useCase: PairingUseCase) -> String {
|
||||||
|
"quickdrop://pair?token=\(token)&usecase=\(useCase.rawValue)&receiver=\(receiverFingerprint)"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func makeQrImage(
|
||||||
|
token: String,
|
||||||
|
receiverFingerprint: String,
|
||||||
|
useCase: PairingUseCase,
|
||||||
|
foregroundColor: CGColor = CGColor(gray: 0, alpha: 1),
|
||||||
|
backgroundColor: CGColor = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
||||||
|
) -> Image? {
|
||||||
|
let payload = qrPayload(token: token, receiverFingerprint: receiverFingerprint, useCase: useCase)
|
||||||
|
do {
|
||||||
|
let qrCodeImage = try QRCode.build
|
||||||
|
.text(payload)
|
||||||
|
.foregroundColor(foregroundColor)
|
||||||
|
.backgroundColor(backgroundColor)
|
||||||
|
.quietZonePixelCount(3)
|
||||||
|
.onPixels.shape(.circle())
|
||||||
|
.eye.shape(.squircle())
|
||||||
|
.errorCorrection(.low)
|
||||||
|
.generate
|
||||||
|
.image(dimension: 1000)
|
||||||
|
return Image(decorative: qrCodeImage, scale: 1.0, orientation: .up)
|
||||||
|
} catch {
|
||||||
|
log("[PairingToken] QR code generation failed: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,17 +5,20 @@
|
|||||||
// Created by Leon Böttger on 22.08.25.
|
// Created by Leon Böttger on 22.08.25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
class TrustStore: ObservableObject {
|
class TrustStore: ObservableObject {
|
||||||
|
|
||||||
static let shared = TrustStore()
|
static let shared = TrustStore()
|
||||||
|
static let trustedCertificatesDidChange = Notification.Name("TrustStore.trustedCertificatesDidChange")
|
||||||
|
|
||||||
@Published private(set) var trustedCertificates: [String: TrustedCertificate] = [:]
|
@Published private(set) var trustedCertificates: [String: TrustedCertificate] = [:]
|
||||||
|
|
||||||
private let trustedKeysKey = "com.leonboettger.quickdrop.identity.trustedKeys"
|
private let trustedKeysKey = "com.leonboettger.quickdrop.identity.trustedKeys"
|
||||||
|
private let pendingPairingQueue = DispatchQueue(label: "TrustStore.pairingPending")
|
||||||
|
private var pendingPairingTrust: [String: PendingPairingTrust] = [:]
|
||||||
|
private let pendingPairingTTL: TimeInterval = 5 * 60
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
loadTrustedCertificates()
|
loadTrustedCertificates()
|
||||||
@@ -25,29 +28,46 @@ class TrustStore: ObservableObject {
|
|||||||
// MARK: - Persistence
|
// MARK: - Persistence
|
||||||
|
|
||||||
private func loadTrustedCertificates() {
|
private func loadTrustedCertificates() {
|
||||||
guard let dict = AppGroup.appGroupUD.dictionary(forKey: trustedKeysKey) as? [String: Data] else {
|
do {
|
||||||
|
if let data = try KeychainStore.loadData(account: trustedKeysKey) {
|
||||||
|
trustedCertificates = try JSONDecoder().decode([String: TrustedCertificate].self, from: data)
|
||||||
|
AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("[TrustStore] Failed to load trusted certificates from keychain: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let dict = legacyTrustedCertificates() else {
|
||||||
trustedCertificates = [:]
|
trustedCertificates = [:]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var decoded: [String: TrustedCertificate] = [:]
|
trustedCertificates = dict
|
||||||
for (key, data) in dict {
|
|
||||||
if let cert = try? JSONDecoder().decode(TrustedCertificate.self, from: data) {
|
do {
|
||||||
decoded[key] = cert
|
try persistTrustedCertificates()
|
||||||
}
|
AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
|
||||||
|
print("[TrustStore] Migrated trusted certificates from shared defaults to keychain.")
|
||||||
|
} catch {
|
||||||
|
print("[TrustStore] Failed to migrate trusted certificates to keychain: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
trustedCertificates = decoded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func saveTrustedCertificates() {
|
private func saveTrustedCertificates() {
|
||||||
var dict: [String: Data] = [:]
|
do {
|
||||||
for (key, cert) in trustedCertificates {
|
try persistTrustedCertificates()
|
||||||
if let data = try? JSONEncoder().encode(cert) {
|
AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
|
||||||
dict[key] = data
|
notifyTrustedCertificatesDidChange()
|
||||||
}
|
} catch {
|
||||||
|
print("[TrustStore] Failed to save trusted certificates to keychain: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
AppGroup.appGroupUD.set(dict, forKey: trustedKeysKey)
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func notifyTrustedCertificatesDidChange() {
|
||||||
|
NotificationCenter.default.post(name: Self.trustedCertificatesDidChange, object: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -58,23 +78,95 @@ class TrustStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func addTrusted(certificate: Sharing_Nearby_PublicCertificate, device: RemoteDeviceInfo) {
|
func addTrusted(
|
||||||
|
certificate: Sharing_Nearby_PublicCertificate,
|
||||||
|
device: RemoteDeviceInfo,
|
||||||
|
grantedUseCases: Set<PairingUseCase> = []
|
||||||
|
) {
|
||||||
guard let publicKeyData = try? certificate.serializedData() else { return }
|
guard let publicKeyData = try? certificate.serializedData() else { return }
|
||||||
|
let fingerprint = certificate.secretID.hex
|
||||||
|
let existingGrants = trustedCertificates[fingerprint]?.grantedUseCases ?? []
|
||||||
|
|
||||||
trustedCertificates[certificate.secretID.hex] = TrustedCertificate(
|
trustedCertificates[fingerprint] = TrustedCertificate(
|
||||||
device: device,
|
device: device,
|
||||||
creationDate: Date(),
|
creationDate: Date(),
|
||||||
certificateData: publicKeyData
|
certificateData: publicKeyData,
|
||||||
|
grantedUseCases: existingGrants.union(grantedUseCases)
|
||||||
)
|
)
|
||||||
|
|
||||||
saveTrustedCertificates()
|
saveTrustedCertificates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func grantUseCase(_ useCase: PairingUseCase, for secretIdHex: String) {
|
||||||
|
let normalized = secretIdHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
guard let existing = trustedCertificates[normalized] else { return }
|
||||||
|
trustedCertificates[normalized] = existing.withGrantedUseCase(useCase)
|
||||||
|
saveTrustedCertificates()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func hasUseCaseGrant(_ useCase: PairingUseCase, for secretIdHex: String?) -> Bool {
|
||||||
|
guard let normalized = secretIdHex?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased(),
|
||||||
|
!normalized.isEmpty else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return trustedCertificates[normalized]?.grantedUseCases.contains(useCase) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func removeTrusted(secretIdHex: String) {
|
func removeTrusted(secretIdHex: String) {
|
||||||
trustedCertificates.removeValue(forKey: secretIdHex)
|
trustedCertificates.removeValue(forKey: secretIdHex)
|
||||||
saveTrustedCertificates()
|
saveTrustedCertificates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Pending Pairing Trust
|
||||||
|
|
||||||
|
func registerPendingPairingTrust(secretIdHex: String, pinCode: String, useCase: PairingUseCase) {
|
||||||
|
let normalized = secretIdHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
let trimmedPin = pinCode.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !normalized.isEmpty, !trimmedPin.isEmpty else { return }
|
||||||
|
|
||||||
|
let pinHash = Self.pinHash(for: trimmedPin)
|
||||||
|
pendingPairingQueue.sync {
|
||||||
|
pendingPairingTrust[normalized] = PendingPairingTrust(
|
||||||
|
useCase: useCase,
|
||||||
|
pinHash: pinHash,
|
||||||
|
createdAt: Date()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmPendingPairingTrust(secretIdHex: String, useCase: PairingUseCase, pinHash: Data) -> Bool {
|
||||||
|
let normalized = secretIdHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
guard !normalized.isEmpty, !pinHash.isEmpty else { return false }
|
||||||
|
|
||||||
|
let now = Date()
|
||||||
|
return pendingPairingQueue.sync {
|
||||||
|
guard let pending = pendingPairingTrust[normalized] else { return false }
|
||||||
|
pendingPairingTrust.removeValue(forKey: normalized)
|
||||||
|
let expired = now.timeIntervalSince(pending.createdAt) > pendingPairingTTL
|
||||||
|
if expired {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard pending.useCase == useCase else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return pending.pinHash == pinHash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearPendingPairingTrust(secretIdHex: String) {
|
||||||
|
let normalized = secretIdHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
guard !normalized.isEmpty else { return }
|
||||||
|
|
||||||
|
_ = pendingPairingQueue.sync {
|
||||||
|
pendingPairingTrust.removeValue(forKey: normalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
struct TrustedCertificate: Codable, Equatable {
|
struct TrustedCertificate: Codable, Equatable {
|
||||||
@@ -83,5 +175,75 @@ class TrustStore: ObservableObject {
|
|||||||
let device: RemoteDeviceInfo
|
let device: RemoteDeviceInfo
|
||||||
let creationDate: Date
|
let creationDate: Date
|
||||||
let certificateData: Data
|
let certificateData: Data
|
||||||
|
let grantedUseCases: Set<PairingUseCase>
|
||||||
|
|
||||||
|
init(
|
||||||
|
device: RemoteDeviceInfo,
|
||||||
|
creationDate: Date,
|
||||||
|
certificateData: Data,
|
||||||
|
grantedUseCases: Set<PairingUseCase> = []
|
||||||
|
) {
|
||||||
|
self.device = device
|
||||||
|
self.creationDate = creationDate
|
||||||
|
self.certificateData = certificateData
|
||||||
|
self.grantedUseCases = grantedUseCases
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case device
|
||||||
|
case creationDate
|
||||||
|
case certificateData
|
||||||
|
case grantedUseCases
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
device = try container.decode(RemoteDeviceInfo.self, forKey: .device)
|
||||||
|
creationDate = try container.decode(Date.self, forKey: .creationDate)
|
||||||
|
certificateData = try container.decode(Data.self, forKey: .certificateData)
|
||||||
|
grantedUseCases = try container.decodeIfPresent(Set<PairingUseCase>.self, forKey: .grantedUseCases) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
func withGrantedUseCase(_ useCase: PairingUseCase) -> TrustedCertificate {
|
||||||
|
TrustedCertificate(
|
||||||
|
device: device,
|
||||||
|
creationDate: creationDate,
|
||||||
|
certificateData: certificateData,
|
||||||
|
grantedUseCases: grantedUseCases.union([useCase])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PendingPairingTrust: Equatable {
|
||||||
|
let useCase: PairingUseCase
|
||||||
|
let pinHash: Data
|
||||||
|
let createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func persistTrustedCertificates() throws {
|
||||||
|
let data = try JSONEncoder().encode(trustedCertificates)
|
||||||
|
try KeychainStore.saveData(data, account: trustedKeysKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func legacyTrustedCertificates() -> [String: TrustedCertificate]? {
|
||||||
|
guard let dict = AppGroup.appGroupUD.dictionary(forKey: trustedKeysKey) as? [String: Data] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded: [String: TrustedCertificate] = [:]
|
||||||
|
for (key, data) in dict {
|
||||||
|
if let cert = try? JSONDecoder().decode(TrustedCertificate.self, from: data) {
|
||||||
|
decoded[key] = cert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func pinHash(for pinCode: String) -> Data {
|
||||||
|
let data = Data(pinCode.utf8)
|
||||||
|
let digest = SHA256.hash(data: data)
|
||||||
|
return Data(digest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,9 +14,29 @@
|
|||||||
699DEBAE2AB0573200115D22 /* QuickDropShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 699DEBA12AB0573200115D22 /* QuickDropShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
699DEBAE2AB0573200115D22 /* QuickDropShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 699DEBA12AB0573200115D22 /* QuickDropShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1129E0BF5100A442DA /* AppDelegate.swift */; };
|
69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1129E0BF5100A442DA /* AppDelegate.swift */; };
|
||||||
69DA9A1429E0BF5200A442DA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; };
|
69DA9A1429E0BF5200A442DA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; };
|
||||||
|
A1B2C3D42F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||||
|
A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||||
|
A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||||
|
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||||
|
A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||||
|
A1B2C3D92F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||||
|
A1C0DE112F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */; };
|
||||||
|
A1C0DE122F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */; };
|
||||||
|
A1C0DE132F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */; };
|
||||||
|
A1C0DE142F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */; };
|
||||||
|
A1F5B3DB2F3A1A0100A1B2C6 /* PairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */; };
|
||||||
|
A1F5B3DC2F3A1A0100A1B2C7 /* PairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */; };
|
||||||
|
A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; };
|
||||||
|
A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; };
|
||||||
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
||||||
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
||||||
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
||||||
|
EB0A22E32FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
|
EB0A22E42FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
|
EB0A22E52FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
|
EB0A22E62FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
|
EB0A22E72FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
|
EB0A22E82FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
||||||
EB1325D42E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
EB1325D42E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
||||||
EB1325D52E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
EB1325D52E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
||||||
@@ -46,7 +66,6 @@
|
|||||||
EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */ = {isa = PBXBuildFile; productRef = EB2F9BFD2DA05F48004010A2 /* BezelNotification */; };
|
EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */ = {isa = PBXBuildFile; productRef = EB2F9BFD2DA05F48004010A2 /* BezelNotification */; };
|
||||||
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB36435F2D73A29F009CDFF8 /* AudioManager.swift */; };
|
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB36435F2D73A29F009CDFF8 /* AudioManager.swift */; };
|
||||||
EB3B9F002EF7150400D6704E /* WelcomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBD2A2162D2860BE00BCB8E2 /* WelcomeScreen.swift */; };
|
EB3B9F002EF7150400D6704E /* WelcomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBD2A2162D2860BE00BCB8E2 /* WelcomeScreen.swift */; };
|
||||||
EB3B9F012EF7150400D6704E /* ChangeDeviceNameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBA1BB1C2EF1F5650068E46E /* ChangeDeviceNameModifier.swift */; };
|
|
||||||
EB3B9F022EF7150400D6704E /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB36435F2D73A29F009CDFF8 /* AudioManager.swift */; };
|
EB3B9F022EF7150400D6704E /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB36435F2D73A29F009CDFF8 /* AudioManager.swift */; };
|
||||||
EB3B9F032EF7150400D6704E /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
EB3B9F032EF7150400D6704E /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
||||||
EB3B9F042EF7150400D6704E /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
EB3B9F042EF7150400D6704E /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
||||||
@@ -71,12 +90,12 @@
|
|||||||
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
||||||
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; };
|
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; };
|
||||||
|
EB3B9F192EF7150400D6704F /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B762E521712002D68FA /* AttachmentDetails.swift */; };
|
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B762E521712002D68FA /* AttachmentDetails.swift */; };
|
||||||
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
||||||
EB3B9F1C2EF7150400D6704E /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
|
EB3B9F1C2EF7150400D6704E /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
|
||||||
EB3B9F1D2EF7150400D6704E /* License.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E662E4F2F7C0052B7D9 /* License.swift */; };
|
EB3B9F1D2EF7150400D6704E /* License.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E662E4F2F7C0052B7D9 /* License.swift */; };
|
||||||
EB3B9F1E2EF7150400D6704E /* LicenseWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54132D78E9E400989564 /* LicenseWindow.swift */; };
|
EB3B9F1E2EF7150400D6704E /* LicenseWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54132D78E9E400989564 /* LicenseWindow.swift */; };
|
||||||
EB3B9F1F2EF7150400D6704E /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
|
||||||
EB3B9F202EF7150400D6704E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1129E0BF5100A442DA /* AppDelegate.swift */; };
|
EB3B9F202EF7150400D6704E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1129E0BF5100A442DA /* AppDelegate.swift */; };
|
||||||
EB3B9F212EF7150400D6704E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */; };
|
EB3B9F212EF7150400D6704E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */; };
|
||||||
EB3B9F222EF7150400D6704E /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54112D78E95C00989564 /* TutorialView.swift */; };
|
EB3B9F222EF7150400D6704E /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54112D78E95C00989564 /* TutorialView.swift */; };
|
||||||
@@ -123,7 +142,6 @@
|
|||||||
EB3B9F5B2EF7151900D6704E /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; };
|
EB3B9F5B2EF7151900D6704E /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; };
|
||||||
EB3B9F5C2EF7151900D6704E /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
EB3B9F5C2EF7151900D6704E /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
||||||
EB3B9F5D2EF7151900D6704E /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
|
EB3B9F5D2EF7151900D6704E /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
|
||||||
EB3B9F5E2EF7151900D6704E /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
|
||||||
EB3B9F5F2EF7151900D6704E /* Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B732E520954002D68FA /* Attachments.swift */; };
|
EB3B9F5F2EF7151900D6704E /* Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B732E520954002D68FA /* Attachments.swift */; };
|
||||||
EB3B9F602EF7151900D6704E /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
EB3B9F602EF7151900D6704E /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
||||||
EB3B9F612EF7151900D6704E /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; };
|
EB3B9F612EF7151900D6704E /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; };
|
||||||
@@ -141,6 +159,7 @@
|
|||||||
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
||||||
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
||||||
|
EB3B9F6F2EF7151900D6704F /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E92E7025D900FD749C /* ApIsolationTester.swift */; };
|
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E92E7025D900FD749C /* ApIsolationTester.swift */; };
|
||||||
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */; };
|
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */; };
|
||||||
EB3B9F722EF7151900D6704E /* ApIsolation6Tester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */; };
|
EB3B9F722EF7151900D6704E /* ApIsolation6Tester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */; };
|
||||||
@@ -160,10 +179,6 @@
|
|||||||
EB3B9F952EF715A000D6704E /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB3B9F942EF715A000D6704E /* QRCode */; };
|
EB3B9F952EF715A000D6704E /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB3B9F942EF715A000D6704E /* QRCode */; };
|
||||||
EB3B9F972EF715A000D6704E /* LUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB3B9F962EF715A000D6704E /* LUI */; };
|
EB3B9F972EF715A000D6704E /* LUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB3B9F962EF715A000D6704E /* LUI */; };
|
||||||
EB3B9F992EF715A000D6704E /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = EB3B9F982EF715A000D6704E /* ZIPFoundation */; };
|
EB3B9F992EF715A000D6704E /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = EB3B9F982EF715A000D6704E /* ZIPFoundation */; };
|
||||||
EB4F9D1F2EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
|
||||||
EB4F9D202EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
|
||||||
EB4F9D212EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
|
||||||
EB4F9D222EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
|
||||||
EB4F9D252EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */; };
|
EB4F9D252EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */; };
|
||||||
EB4F9D272EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */; };
|
EB4F9D272EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */; };
|
||||||
EB53D8C42EF496AB004C95BA /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = EB53D8C32EF496AB004C95BA /* ZIPFoundation */; };
|
EB53D8C42EF496AB004C95BA /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = EB53D8C32EF496AB004C95BA /* ZIPFoundation */; };
|
||||||
@@ -196,11 +211,13 @@
|
|||||||
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
|
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
|
||||||
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
||||||
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
|
EB5C37CF2E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB5C37D02E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
|
EB5C37D02E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
|
||||||
EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
||||||
EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
||||||
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
||||||
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
|
EB5C37D42E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D52E377ACF0052EFD7 /* SwiftProtobuf */; };
|
EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D52E377ACF0052EFD7 /* SwiftProtobuf */; };
|
||||||
EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D72E377ACF0052EFD7 /* SwiftECC */; };
|
EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D72E377ACF0052EFD7 /* SwiftECC */; };
|
||||||
EB5C37DA2E377AD60052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */; };
|
EB5C37DA2E377AD60052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */; };
|
||||||
@@ -210,6 +227,7 @@
|
|||||||
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
||||||
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
||||||
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
|
EB5C37F42E377CE70052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
||||||
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
|
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
|
||||||
EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
||||||
@@ -228,6 +246,11 @@
|
|||||||
EB5C383F2E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
EB5C383F2E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
||||||
EB5C38402E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
EB5C38402E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
||||||
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
||||||
|
EB6E51A52F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; };
|
||||||
|
EB6E51A62F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; };
|
||||||
|
EB6E51AB2F311ED4006D9812 /* QuickDropIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = EB721F232E75AFA900F2099F /* QuickDropIcon.icon */; };
|
||||||
|
EB6E51AC2F311ED4006D9812 /* QuickDropIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = EB721F232E75AFA900F2099F /* QuickDropIcon.icon */; };
|
||||||
|
EB6E51AD2F311ED4006D9812 /* QuickDropIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = EB721F232E75AFA900F2099F /* QuickDropIcon.icon */; };
|
||||||
EB6E6AB42ECBA46D00E71D57 /* PhotoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBBEE1452EC9D92F00AA7831 /* PhotoManager.swift */; };
|
EB6E6AB42ECBA46D00E71D57 /* PhotoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBBEE1452EC9D92F00AA7831 /* PhotoManager.swift */; };
|
||||||
EB6E6B212ECBA95000E71D57 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */; };
|
EB6E6B212ECBA95000E71D57 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */; };
|
||||||
EB6E6B222ECBA95000E71D57 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */; };
|
EB6E6B222ECBA95000E71D57 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */; };
|
||||||
@@ -264,7 +287,6 @@
|
|||||||
EB950B3E2E52038A002D68FA /* SendModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F902E50CA23008B8C5C /* SendModel.swift */; };
|
EB950B3E2E52038A002D68FA /* SendModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F902E50CA23008B8C5C /* SendModel.swift */; };
|
||||||
EB950B3F2E52038A002D68FA /* FilePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8F2E50CA23008B8C5C /* FilePicker.swift */; };
|
EB950B3F2E52038A002D68FA /* FilePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8F2E50CA23008B8C5C /* FilePicker.swift */; };
|
||||||
EB950B402E52038A002D68FA /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; };
|
EB950B402E52038A002D68FA /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; };
|
||||||
EB950B412E52038A002D68FA /* PieProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F992E50CAA6008B8C5C /* PieProgressView.swift */; };
|
|
||||||
EB950B432E52038A002D68FA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8E2E50CA23008B8C5C /* ContentView.swift */; };
|
EB950B432E52038A002D68FA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8E2E50CA23008B8C5C /* ContentView.swift */; };
|
||||||
EB950B442E520414002D68FA /* License.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E662E4F2F7C0052B7D9 /* License.swift */; };
|
EB950B442E520414002D68FA /* License.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E662E4F2F7C0052B7D9 /* License.swift */; };
|
||||||
EB950B452E520436002D68FA /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
EB950B452E520436002D68FA /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
||||||
@@ -278,6 +300,7 @@
|
|||||||
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; };
|
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; };
|
||||||
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
||||||
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
|
EB950B522E520436002D68FB /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
||||||
EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
||||||
EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
||||||
@@ -307,7 +330,6 @@
|
|||||||
EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8F2E50CA23008B8C5C /* FilePicker.swift */; };
|
EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8F2E50CA23008B8C5C /* FilePicker.swift */; };
|
||||||
EB979F972E50CA23008B8C5C /* SendModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F902E50CA23008B8C5C /* SendModel.swift */; };
|
EB979F972E50CA23008B8C5C /* SendModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F902E50CA23008B8C5C /* SendModel.swift */; };
|
||||||
EB979F982E50CA23008B8C5C /* QuickDropiOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */; };
|
EB979F982E50CA23008B8C5C /* QuickDropiOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */; };
|
||||||
EB979F9A2E50CAA6008B8C5C /* PieProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F992E50CAA6008B8C5C /* PieProgressView.swift */; };
|
|
||||||
EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB97A00E2E511120008B8C5C /* ReceiveModel.swift */; };
|
EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB97A00E2E511120008B8C5C /* ReceiveModel.swift */; };
|
||||||
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB97A00E2E511120008B8C5C /* ReceiveModel.swift */; };
|
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB97A00E2E511120008B8C5C /* ReceiveModel.swift */; };
|
||||||
EB97A0142E51DD00008B8C5C /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
EB97A0142E51DD00008B8C5C /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
||||||
@@ -319,7 +341,6 @@
|
|||||||
EB9925852E45EF0900357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925842E45EF0900357055 /* QRCode */; };
|
EB9925852E45EF0900357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925842E45EF0900357055 /* QRCode */; };
|
||||||
EB9925872E45F12F00357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925862E45F12F00357055 /* QRCode */; };
|
EB9925872E45F12F00357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925862E45F12F00357055 /* QRCode */; };
|
||||||
EB9925892E45F13500357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925882E45F13500357055 /* QRCode */; };
|
EB9925892E45F13500357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925882E45F13500357055 /* QRCode */; };
|
||||||
EBA1BB1D2EF1F5650068E46E /* ChangeDeviceNameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBA1BB1C2EF1F5650068E46E /* ChangeDeviceNameModifier.swift */; };
|
|
||||||
EBA1BB1E2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBA1BB1C2EF1F5650068E46E /* ChangeDeviceNameModifier.swift */; };
|
EBA1BB1E2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBA1BB1C2EF1F5650068E46E /* ChangeDeviceNameModifier.swift */; };
|
||||||
EBA1BB1F2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBA1BB1C2EF1F5650068E46E /* ChangeDeviceNameModifier.swift */; };
|
EBA1BB1F2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBA1BB1C2EF1F5650068E46E /* ChangeDeviceNameModifier.swift */; };
|
||||||
EBADEB8F2E8029750021B5E4 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = EBADEB8E2E8029750021B5E4 /* DeviceKit */; };
|
EBADEB8F2E8029750021B5E4 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = EBADEB8E2E8029750021B5E4 /* DeviceKit */; };
|
||||||
@@ -497,7 +518,13 @@
|
|||||||
69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = offline_wire_formats.pb.swift; sourceTree = "<group>"; };
|
69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = offline_wire_formats.pb.swift; sourceTree = "<group>"; };
|
||||||
69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securegcm.pb.swift; sourceTree = "<group>"; };
|
69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securegcm.pb.swift; sourceTree = "<group>"; };
|
||||||
69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securemessage.pb.swift; sourceTree = "<group>"; };
|
69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securemessage.pb.swift; sourceTree = "<group>"; };
|
||||||
|
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MirroredNotificationSupport.swift; sourceTree = "<group>"; };
|
||||||
|
A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncNotificationPresenter.swift; sourceTree = "<group>"; };
|
||||||
|
A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingToken.swift; sourceTree = "<group>"; };
|
||||||
|
A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingQrSheetManager.swift; sourceTree = "<group>"; };
|
||||||
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedDevicesView.swift; sourceTree = "<group>"; };
|
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedDevicesView.swift; sourceTree = "<group>"; };
|
||||||
|
EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyBonjourServiceResolver.swift; sourceTree = "<group>"; };
|
||||||
EB1325D22E701D8900FD749C /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
EB1325D22E701D8900FD749C /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||||
EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveNetworkInterface.swift; sourceTree = "<group>"; };
|
EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveNetworkInterface.swift; sourceTree = "<group>"; };
|
||||||
EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApIsolation6Tester.swift; sourceTree = "<group>"; };
|
EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApIsolation6Tester.swift; sourceTree = "<group>"; };
|
||||||
@@ -510,11 +537,11 @@
|
|||||||
EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; };
|
EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; };
|
||||||
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
|
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
|
||||||
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
|
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
|
||||||
|
EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDeviceNameResolver.swift; sourceTree = "<group>"; };
|
||||||
EB3B9F482EF7150400D6704E /* QuickDropGitHub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropGitHub.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
EB3B9F482EF7150400D6704E /* QuickDropGitHub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropGitHub.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EB3B9F492EF7150400D6704E /* QuickDrop copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "QuickDrop copy-Info.plist"; path = "/Users/leonboettger/QuickDrop Backup with git/QuickDrop copy-Info.plist"; sourceTree = "<absolute>"; };
|
EB3B9F492EF7150400D6704E /* QuickDrop copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "QuickDrop copy-Info.plist"; path = "/Users/leonboettger/QuickDrop Backup with git/QuickDrop copy-Info.plist"; sourceTree = "<absolute>"; };
|
||||||
EB3B9F892EF7151900D6704E /* QuickDropShareExtensionGitHub.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QuickDropShareExtensionGitHub.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
EB3B9F892EF7151900D6704E /* QuickDropShareExtensionGitHub.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QuickDropShareExtensionGitHub.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EB3B9F8A2EF7151900D6704E /* QuickDropShareExtension copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "QuickDropShareExtension copy-Info.plist"; path = "/Users/leonboettger/QuickDrop Backup with git/QuickDropShareExtension copy-Info.plist"; sourceTree = "<absolute>"; };
|
EB3B9F8A2EF7151900D6704E /* QuickDropShareExtension copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "QuickDropShareExtension copy-Info.plist"; path = "/Users/leonboettger/QuickDrop Backup with git/QuickDropShareExtension copy-Info.plist"; sourceTree = "<absolute>"; };
|
||||||
EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXIFUtils.swift; sourceTree = "<group>"; };
|
|
||||||
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+DocumentsOpener.swift"; sourceTree = "<group>"; };
|
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+DocumentsOpener.swift"; sourceTree = "<group>"; };
|
||||||
EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
EB5C36952E34EBBD0052EFD7 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/ShareViewController.xcstrings; sourceTree = "<group>"; };
|
EB5C36952E34EBBD0052EFD7 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/ShareViewController.xcstrings; sourceTree = "<group>"; };
|
||||||
@@ -523,6 +550,7 @@
|
|||||||
EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; };
|
EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; };
|
||||||
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; };
|
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; };
|
||||||
|
EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionSheetManager.swift; sourceTree = "<group>"; };
|
||||||
EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
||||||
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
EB6FE7CC2EFEAFF000FF2BCF /* NoLocalNetworkAccessAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoLocalNetworkAccessAlertView.swift; sourceTree = "<group>"; };
|
EB6FE7CC2EFEAFF000FF2BCF /* NoLocalNetworkAccessAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoLocalNetworkAccessAlertView.swift; sourceTree = "<group>"; };
|
||||||
@@ -548,7 +576,6 @@
|
|||||||
EB979F902E50CA23008B8C5C /* SendModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendModel.swift; sourceTree = "<group>"; };
|
EB979F902E50CA23008B8C5C /* SendModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendModel.swift; sourceTree = "<group>"; };
|
||||||
EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickDropiOS.entitlements; sourceTree = "<group>"; };
|
EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickDropiOS.entitlements; sourceTree = "<group>"; };
|
||||||
EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickDropiOSApp.swift; sourceTree = "<group>"; };
|
EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickDropiOSApp.swift; sourceTree = "<group>"; };
|
||||||
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PieProgressView.swift; sourceTree = "<group>"; };
|
|
||||||
EB97A00E2E511120008B8C5C /* ReceiveModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveModel.swift; sourceTree = "<group>"; };
|
EB97A00E2E511120008B8C5C /* ReceiveModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveModel.swift; sourceTree = "<group>"; };
|
||||||
EB97A0162E51DF4F008B8C5C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
EB97A0162E51DF4F008B8C5C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlert.swift; sourceTree = "<group>"; };
|
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlert.swift; sourceTree = "<group>"; };
|
||||||
@@ -706,6 +733,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
69DA9A1129E0BF5100A442DA /* AppDelegate.swift */,
|
69DA9A1129E0BF5100A442DA /* AppDelegate.swift */,
|
||||||
|
EB979F8D2E50CA23008B8C5C /* Configuration.swift */,
|
||||||
EB1325D22E701D8900FD749C /* Settings.swift */,
|
EB1325D22E701D8900FD749C /* Settings.swift */,
|
||||||
EB644B852EA507CF006C2E6C /* ViewModel */,
|
EB644B852EA507CF006C2E6C /* ViewModel */,
|
||||||
EBC05C6A2E33B55100EECC18 /* View */,
|
EBC05C6A2E33B55100EECC18 /* View */,
|
||||||
@@ -742,6 +770,14 @@
|
|||||||
path = NearbyShare;
|
path = NearbyShare;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A1F5B3DA2F3A1A0100A1B2C5 /* Pairing */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */,
|
||||||
|
);
|
||||||
|
path = Pairing;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
EB1325EA2E7025D900FD749C /* SelfTest */ = {
|
EB1325EA2E7025D900FD749C /* SelfTest */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -762,8 +798,8 @@
|
|||||||
EB16B7762E59BA9F007D9792 /* AppGroup.swift */,
|
EB16B7762E59BA9F007D9792 /* AppGroup.swift */,
|
||||||
EB886E512E4E19390052B7D9 /* HKDF.swift */,
|
EB886E512E4E19390052B7D9 /* HKDF.swift */,
|
||||||
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */,
|
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */,
|
||||||
EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */,
|
|
||||||
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
|
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
|
||||||
|
A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -772,7 +808,10 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */,
|
69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */,
|
||||||
|
EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */,
|
||||||
|
EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */,
|
||||||
69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */,
|
69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */,
|
||||||
|
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */,
|
||||||
691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */,
|
691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */,
|
||||||
698DFAE529E2F91A0064F247 /* NearbyConnection.swift */,
|
698DFAE529E2F91A0064F247 /* NearbyConnection.swift */,
|
||||||
);
|
);
|
||||||
@@ -812,12 +851,12 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
EB979F8E2E50CA23008B8C5C /* ContentView.swift */,
|
EB979F8E2E50CA23008B8C5C /* ContentView.swift */,
|
||||||
|
EBA1BB1C2EF1F5650068E46E /* ChangeDeviceNameModifier.swift */,
|
||||||
EB6FE7CC2EFEAFF000FF2BCF /* NoLocalNetworkAccessAlertView.swift */,
|
EB6FE7CC2EFEAFF000FF2BCF /* NoLocalNetworkAccessAlertView.swift */,
|
||||||
EB979F8F2E50CA23008B8C5C /* FilePicker.swift */,
|
EB979F8F2E50CA23008B8C5C /* FilePicker.swift */,
|
||||||
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */,
|
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */,
|
||||||
EBBEE1452EC9D92F00AA7831 /* PhotoManager.swift */,
|
EBBEE1452EC9D92F00AA7831 /* PhotoManager.swift */,
|
||||||
EBBC93B12EC8B75F00F541A1 /* LocalNetworkPermissionView.swift */,
|
EBBC93B12EC8B75F00F541A1 /* LocalNetworkPermissionView.swift */,
|
||||||
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */,
|
|
||||||
);
|
);
|
||||||
path = View;
|
path = View;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -830,6 +869,42 @@
|
|||||||
path = Helpers;
|
path = Helpers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
EB6E51A72F2F95D0006D9812 /* Introduction */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */,
|
||||||
|
EB7A95F22EF6E1BE0021111B /* IntroductionView.swift */,
|
||||||
|
);
|
||||||
|
path = Introduction;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
EB6E51A82F2F95E0006D9812 /* Settings */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */,
|
||||||
|
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */,
|
||||||
|
);
|
||||||
|
path = Settings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
EB6E51A92F2F95EE006D9812 /* UserManual */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
EBD2A2162D2860BE00BCB8E2 /* WelcomeScreen.swift */,
|
||||||
|
EB7F54112D78E95C00989564 /* TutorialView.swift */,
|
||||||
|
EB7F54132D78E9E400989564 /* LicenseWindow.swift */,
|
||||||
|
);
|
||||||
|
path = UserManual;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
EB6E51AA2F2F9606006D9812 /* Toast */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */,
|
||||||
|
);
|
||||||
|
path = Toast;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
EB950B6C2E5206E2002D68FA /* iOSShareExtension */ = {
|
EB950B6C2E5206E2002D68FA /* iOSShareExtension */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -845,7 +920,6 @@
|
|||||||
children = (
|
children = (
|
||||||
EB97A0162E51DF4F008B8C5C /* Info.plist */,
|
EB97A0162E51DF4F008B8C5C /* Info.plist */,
|
||||||
EB1325FC2E7034B000FD749C /* InfoPlist.xcstrings */,
|
EB1325FC2E7034B000FD749C /* InfoPlist.xcstrings */,
|
||||||
EB979F8D2E50CA23008B8C5C /* Configuration.swift */,
|
|
||||||
EB644B862EA50807006C2E6C /* View */,
|
EB644B862EA50807006C2E6C /* View */,
|
||||||
EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */,
|
EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */,
|
||||||
EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */,
|
EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */,
|
||||||
@@ -857,14 +931,11 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
EB7654772E525ACC006B4AA3 /* MainMenu.xib */,
|
EB7654772E525ACC006B4AA3 /* MainMenu.xib */,
|
||||||
EBD2A2162D2860BE00BCB8E2 /* WelcomeScreen.swift */,
|
EB6E51A92F2F95EE006D9812 /* UserManual */,
|
||||||
EB7F54112D78E95C00989564 /* TutorialView.swift */,
|
EB6E51AA2F2F9606006D9812 /* Toast */,
|
||||||
EB7F54132D78E9E400989564 /* LicenseWindow.swift */,
|
A1F5B3DA2F3A1A0100A1B2C5 /* Pairing */,
|
||||||
EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */,
|
EB6E51A82F2F95E0006D9812 /* Settings */,
|
||||||
EBA1BB1C2EF1F5650068E46E /* ChangeDeviceNameModifier.swift */,
|
EB6E51A72F2F95D0006D9812 /* Introduction */,
|
||||||
EB7A95F22EF6E1BE0021111B /* IntroductionView.swift */,
|
|
||||||
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */,
|
|
||||||
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */,
|
|
||||||
EBC05C812E33B58300EECC18 /* IssueViews */,
|
EBC05C812E33B58300EECC18 /* IssueViews */,
|
||||||
);
|
);
|
||||||
path = View;
|
path = View;
|
||||||
@@ -873,6 +944,8 @@
|
|||||||
EBC05C6B2E33B57100EECC18 /* Util */ = {
|
EBC05C6B2E33B57100EECC18 /* Util */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */,
|
||||||
|
A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */,
|
||||||
EB1325F72E70287C00FD749C /* SendToExtension.swift */,
|
EB1325F72E70287C00FD749C /* SendToExtension.swift */,
|
||||||
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */,
|
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */,
|
||||||
EB886E662E4F2F7C0052B7D9 /* License.swift */,
|
EB886E662E4F2F7C0052B7D9 /* License.swift */,
|
||||||
@@ -1134,6 +1207,7 @@
|
|||||||
files = (
|
files = (
|
||||||
EBD6AFBC2EF826E600DED4CD /* ConfigSynced.storekit in Resources */,
|
EBD6AFBC2EF826E600DED4CD /* ConfigSynced.storekit in Resources */,
|
||||||
EB5C36942E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */,
|
EB5C36942E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */,
|
||||||
|
EB6E51AB2F311ED4006D9812 /* QuickDropIcon.icon in Resources */,
|
||||||
EB5C36E52E34EC0E0052EFD7 /* License.txt in Resources */,
|
EB5C36E52E34EC0E0052EFD7 /* License.txt in Resources */,
|
||||||
691F53BC2ABB70840089FD92 /* DeviceListCell.xib in Resources */,
|
691F53BC2ABB70840089FD92 /* DeviceListCell.xib in Resources */,
|
||||||
699DEBA92AB0573200115D22 /* ShareViewController.xib in Resources */,
|
699DEBA92AB0573200115D22 /* ShareViewController.xib in Resources */,
|
||||||
@@ -1182,6 +1256,7 @@
|
|||||||
files = (
|
files = (
|
||||||
EBD6AFC02EF826E600DED4CD /* ConfigSynced.storekit in Resources */,
|
EBD6AFC02EF826E600DED4CD /* ConfigSynced.storekit in Resources */,
|
||||||
EB3B9F7D2EF7151900D6704E /* Localizable.xcstrings in Resources */,
|
EB3B9F7D2EF7151900D6704E /* Localizable.xcstrings in Resources */,
|
||||||
|
EB6E51AD2F311ED4006D9812 /* QuickDropIcon.icon in Resources */,
|
||||||
EB3B9F7E2EF7151900D6704E /* License.txt in Resources */,
|
EB3B9F7E2EF7151900D6704E /* License.txt in Resources */,
|
||||||
EB3B9F7F2EF7151900D6704E /* DeviceListCell.xib in Resources */,
|
EB3B9F7F2EF7151900D6704E /* DeviceListCell.xib in Resources */,
|
||||||
EB3B9F802EF7151900D6704E /* ShareViewController.xib in Resources */,
|
EB3B9F802EF7151900D6704E /* ShareViewController.xib in Resources */,
|
||||||
@@ -1215,6 +1290,7 @@
|
|||||||
EB1325FE2E7034B000FD749C /* InfoPlist.xcstrings in Resources */,
|
EB1325FE2E7034B000FD749C /* InfoPlist.xcstrings in Resources */,
|
||||||
EB950B702E520734002D68FA /* Localizable.xcstrings in Resources */,
|
EB950B702E520734002D68FA /* Localizable.xcstrings in Resources */,
|
||||||
EBD6AFBE2EF826E600DED4CD /* ConfigSynced.storekit in Resources */,
|
EBD6AFBE2EF826E600DED4CD /* ConfigSynced.storekit in Resources */,
|
||||||
|
EB6E51AC2F311ED4006D9812 /* QuickDropIcon.icon in Resources */,
|
||||||
EB950B5C2E52065A002D68FA /* justMaybe.mp3 in Resources */,
|
EB950B5C2E52065A002D68FA /* justMaybe.mp3 in Resources */,
|
||||||
EB950B5D2E52065A002D68FA /* License.txt in Resources */,
|
EB950B5D2E52065A002D68FA /* License.txt in Resources */,
|
||||||
EB7654C12E534A8E006B4AA3 /* Assets.xcassets in Resources */,
|
EB7654C12E534A8E006B4AA3 /* Assets.xcassets in Resources */,
|
||||||
@@ -1244,9 +1320,9 @@
|
|||||||
EB5C37C52E377A780052EFD7 /* securemessage.pb.swift in Sources */,
|
EB5C37C52E377A780052EFD7 /* securemessage.pb.swift in Sources */,
|
||||||
EB5C37C62E377A780052EFD7 /* securegcm.pb.swift in Sources */,
|
EB5C37C62E377A780052EFD7 /* securegcm.pb.swift in Sources */,
|
||||||
EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
||||||
|
EB0A22E42FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
||||||
EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
||||||
EB4F9D222EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
|
||||||
EB950B742E520957002D68FA /* Attachments.swift in Sources */,
|
EB950B742E520957002D68FA /* Attachments.swift in Sources */,
|
||||||
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */,
|
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */,
|
||||||
EB16B7782E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
EB16B7782E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
||||||
@@ -1262,7 +1338,9 @@
|
|||||||
EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */,
|
EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */,
|
||||||
EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */,
|
EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */,
|
||||||
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
||||||
|
A1B2C3D42F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||||
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB5C37D42E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */,
|
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */,
|
||||||
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
||||||
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
||||||
@@ -1276,7 +1354,6 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
EBD2A2172D2860C100BCB8E2 /* WelcomeScreen.swift in Sources */,
|
EBD2A2172D2860C100BCB8E2 /* WelcomeScreen.swift in Sources */,
|
||||||
EBA1BB1D2EF1F5650068E46E /* ChangeDeviceNameModifier.swift in Sources */,
|
|
||||||
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */,
|
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */,
|
||||||
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */,
|
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */,
|
||||||
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */,
|
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */,
|
||||||
@@ -1295,19 +1372,24 @@
|
|||||||
EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||||
EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */,
|
EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */,
|
||||||
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */,
|
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */,
|
||||||
|
A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */,
|
||||||
|
A1F5B3DB2F3A1A0100A1B2C6 /* PairingToken.swift in Sources */,
|
||||||
EB5C37CB2E377A7F0052EFD7 /* NearbyConnection.swift in Sources */,
|
EB5C37CB2E377A7F0052EFD7 /* NearbyConnection.swift in Sources */,
|
||||||
EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */,
|
EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */,
|
||||||
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
||||||
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
||||||
|
A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||||
|
A1C0DE132F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */,
|
||||||
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB5C37CF2E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EBE5FF422EDB6F6F0019B380 /* Configuration.swift in Sources */,
|
EBE5FF422EDB6F6F0019B380 /* Configuration.swift in Sources */,
|
||||||
EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */,
|
EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */,
|
||||||
EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */,
|
EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */,
|
||||||
EB96ED8C2D7F7E7B004B3FF1 /* SmallSheetView.swift in Sources */,
|
EB96ED8C2D7F7E7B004B3FF1 /* SmallSheetView.swift in Sources */,
|
||||||
EB886E682E4F2F800052B7D9 /* License.swift in Sources */,
|
EB886E682E4F2F800052B7D9 /* License.swift in Sources */,
|
||||||
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */,
|
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */,
|
||||||
EB4F9D202EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
|
||||||
69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */,
|
69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */,
|
||||||
|
EB6E51A62F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */,
|
||||||
EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */,
|
EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */,
|
||||||
EB7F54122D78E96000989564 /* TutorialView.swift in Sources */,
|
EB7F54122D78E96000989564 /* TutorialView.swift in Sources */,
|
||||||
EB9925812E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
EB9925812E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
||||||
@@ -1317,12 +1399,14 @@
|
|||||||
EBC05C822E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */,
|
EBC05C822E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */,
|
||||||
EBC05C832E33B58300EECC18 /* FirewallIssueView.swift in Sources */,
|
EBC05C832E33B58300EECC18 /* FirewallIssueView.swift in Sources */,
|
||||||
EB1325FA2E70288300FD749C /* SendToExtension.swift in Sources */,
|
EB1325FA2E70288300FD749C /* SendToExtension.swift in Sources */,
|
||||||
|
EB0A22E72FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
EB1325F42E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
EB1325F42E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
||||||
EB1325F52E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
EB1325F52E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
||||||
EB1325F62E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
|
EB1325F62E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
|
||||||
EBC05C842E33B58300EECC18 /* IssueView.swift in Sources */,
|
EBC05C842E33B58300EECC18 /* IssueView.swift in Sources */,
|
||||||
EBC05C852E33B58300EECC18 /* NetworkFilterIssueView.swift in Sources */,
|
EBC05C852E33B58300EECC18 /* NetworkFilterIssueView.swift in Sources */,
|
||||||
EB7A95F42EF6E1BE0021111B /* IntroductionView.swift in Sources */,
|
EB7A95F42EF6E1BE0021111B /* IntroductionView.swift in Sources */,
|
||||||
|
A1C0DE112F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -1331,7 +1415,6 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
EB3B9F002EF7150400D6704E /* WelcomeScreen.swift in Sources */,
|
EB3B9F002EF7150400D6704E /* WelcomeScreen.swift in Sources */,
|
||||||
EB3B9F012EF7150400D6704E /* ChangeDeviceNameModifier.swift in Sources */,
|
|
||||||
EB3B9F022EF7150400D6704E /* AudioManager.swift in Sources */,
|
EB3B9F022EF7150400D6704E /* AudioManager.swift in Sources */,
|
||||||
EB3B9F032EF7150400D6704E /* LaunchAtLogin.swift in Sources */,
|
EB3B9F032EF7150400D6704E /* LaunchAtLogin.swift in Sources */,
|
||||||
EB3B9F042EF7150400D6704E /* SleepManager.swift in Sources */,
|
EB3B9F042EF7150400D6704E /* SleepManager.swift in Sources */,
|
||||||
@@ -1350,19 +1433,24 @@
|
|||||||
EB3B9F112EF7150400D6704E /* IdentityManager.swift in Sources */,
|
EB3B9F112EF7150400D6704E /* IdentityManager.swift in Sources */,
|
||||||
EB3B9F122EF7150400D6704E /* securegcm.pb.swift in Sources */,
|
EB3B9F122EF7150400D6704E /* securegcm.pb.swift in Sources */,
|
||||||
EB3B9F132EF7150400D6704E /* ReceiveModel.swift in Sources */,
|
EB3B9F132EF7150400D6704E /* ReceiveModel.swift in Sources */,
|
||||||
|
A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */,
|
||||||
|
A1F5B3DC2F3A1A0100A1B2C7 /* PairingToken.swift in Sources */,
|
||||||
EB3B9F142EF7150400D6704E /* NearbyConnection.swift in Sources */,
|
EB3B9F142EF7150400D6704E /* NearbyConnection.swift in Sources */,
|
||||||
EB3B9F152EF7150400D6704E /* DataModel.swift in Sources */,
|
EB3B9F152EF7150400D6704E /* DataModel.swift in Sources */,
|
||||||
EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */,
|
EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */,
|
||||||
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */,
|
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */,
|
||||||
|
A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||||
|
A1C0DE142F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */,
|
||||||
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */,
|
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB3B9F192EF7150400D6704F /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */,
|
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */,
|
||||||
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */,
|
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */,
|
||||||
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */,
|
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */,
|
||||||
EB3B9F1C2EF7150400D6704E /* SmallSheetView.swift in Sources */,
|
EB3B9F1C2EF7150400D6704E /* SmallSheetView.swift in Sources */,
|
||||||
EB3B9F1D2EF7150400D6704E /* License.swift in Sources */,
|
EB3B9F1D2EF7150400D6704E /* License.swift in Sources */,
|
||||||
EB3B9F1E2EF7150400D6704E /* LicenseWindow.swift in Sources */,
|
EB3B9F1E2EF7150400D6704E /* LicenseWindow.swift in Sources */,
|
||||||
EB3B9F1F2EF7150400D6704E /* EXIFUtils.swift in Sources */,
|
|
||||||
EB3B9F202EF7150400D6704E /* AppDelegate.swift in Sources */,
|
EB3B9F202EF7150400D6704E /* AppDelegate.swift in Sources */,
|
||||||
|
EB6E51A52F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */,
|
||||||
EB3B9F212EF7150400D6704E /* SettingsView.swift in Sources */,
|
EB3B9F212EF7150400D6704E /* SettingsView.swift in Sources */,
|
||||||
EB3B9F222EF7150400D6704E /* TutorialView.swift in Sources */,
|
EB3B9F222EF7150400D6704E /* TutorialView.swift in Sources */,
|
||||||
EB3B9F232EF7150400D6704E /* sharing_enums.pb.swift in Sources */,
|
EB3B9F232EF7150400D6704E /* sharing_enums.pb.swift in Sources */,
|
||||||
@@ -1372,12 +1460,14 @@
|
|||||||
EB3B9F272EF7150400D6704E /* ApIsolationIssueView.swift in Sources */,
|
EB3B9F272EF7150400D6704E /* ApIsolationIssueView.swift in Sources */,
|
||||||
EB3B9F282EF7150400D6704E /* FirewallIssueView.swift in Sources */,
|
EB3B9F282EF7150400D6704E /* FirewallIssueView.swift in Sources */,
|
||||||
EB3B9F292EF7150400D6704E /* SendToExtension.swift in Sources */,
|
EB3B9F292EF7150400D6704E /* SendToExtension.swift in Sources */,
|
||||||
|
EB0A22E82FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
EB3B9F2A2EF7150400D6704E /* ApIsolationTester.swift in Sources */,
|
EB3B9F2A2EF7150400D6704E /* ApIsolationTester.swift in Sources */,
|
||||||
EB3B9F2B2EF7150400D6704E /* ActiveNetworkInterface.swift in Sources */,
|
EB3B9F2B2EF7150400D6704E /* ActiveNetworkInterface.swift in Sources */,
|
||||||
EB3B9F2C2EF7150400D6704E /* ApIsolation6Tester.swift in Sources */,
|
EB3B9F2C2EF7150400D6704E /* ApIsolation6Tester.swift in Sources */,
|
||||||
EB3B9F2D2EF7150400D6704E /* IssueView.swift in Sources */,
|
EB3B9F2D2EF7150400D6704E /* IssueView.swift in Sources */,
|
||||||
EB3B9F2E2EF7150400D6704E /* NetworkFilterIssueView.swift in Sources */,
|
EB3B9F2E2EF7150400D6704E /* NetworkFilterIssueView.swift in Sources */,
|
||||||
EB3B9F2F2EF7150400D6704E /* IntroductionView.swift in Sources */,
|
EB3B9F2F2EF7150400D6704E /* IntroductionView.swift in Sources */,
|
||||||
|
A1C0DE122F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -1401,9 +1491,9 @@
|
|||||||
EB3B9F592EF7151900D6704E /* securemessage.pb.swift in Sources */,
|
EB3B9F592EF7151900D6704E /* securemessage.pb.swift in Sources */,
|
||||||
EB3B9F5A2EF7151900D6704E /* securegcm.pb.swift in Sources */,
|
EB3B9F5A2EF7151900D6704E /* securegcm.pb.swift in Sources */,
|
||||||
EB3B9F5B2EF7151900D6704E /* wire_format.pb.swift in Sources */,
|
EB3B9F5B2EF7151900D6704E /* wire_format.pb.swift in Sources */,
|
||||||
|
EB0A22E52FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
EB3B9F5C2EF7151900D6704E /* ukey.pb.swift in Sources */,
|
EB3B9F5C2EF7151900D6704E /* ukey.pb.swift in Sources */,
|
||||||
EB3B9F5D2EF7151900D6704E /* device_to_device_messages.pb.swift in Sources */,
|
EB3B9F5D2EF7151900D6704E /* device_to_device_messages.pb.swift in Sources */,
|
||||||
EB3B9F5E2EF7151900D6704E /* EXIFUtils.swift in Sources */,
|
|
||||||
EB3B9F5F2EF7151900D6704E /* Attachments.swift in Sources */,
|
EB3B9F5F2EF7151900D6704E /* Attachments.swift in Sources */,
|
||||||
EB3B9F602EF7151900D6704E /* Settings.swift in Sources */,
|
EB3B9F602EF7151900D6704E /* Settings.swift in Sources */,
|
||||||
EB3B9F612EF7151900D6704E /* AppGroup.swift in Sources */,
|
EB3B9F612EF7151900D6704E /* AppGroup.swift in Sources */,
|
||||||
@@ -1419,7 +1509,9 @@
|
|||||||
EB3B9F6B2EF7151900D6704E /* DataModel.swift in Sources */,
|
EB3B9F6B2EF7151900D6704E /* DataModel.swift in Sources */,
|
||||||
EB3B9F6C2EF7151900D6704E /* NearbyConnection.swift in Sources */,
|
EB3B9F6C2EF7151900D6704E /* NearbyConnection.swift in Sources */,
|
||||||
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */,
|
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */,
|
||||||
|
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||||
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */,
|
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB3B9F6F2EF7151900D6704F /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */,
|
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */,
|
||||||
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */,
|
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */,
|
||||||
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */,
|
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */,
|
||||||
@@ -1440,6 +1532,7 @@
|
|||||||
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||||
EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
|
EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
|
||||||
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB5C37F42E377CE70052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */,
|
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */,
|
||||||
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
||||||
EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */,
|
EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */,
|
||||||
@@ -1447,13 +1540,14 @@
|
|||||||
EB5C37F92E377CE70052EFD7 /* securegcm.pb.swift in Sources */,
|
EB5C37F92E377CE70052EFD7 /* securegcm.pb.swift in Sources */,
|
||||||
EB5C37FA2E377CE70052EFD7 /* Helpers.swift in Sources */,
|
EB5C37FA2E377CE70052EFD7 /* Helpers.swift in Sources */,
|
||||||
EB5C37FB2E377CE70052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
EB5C37FB2E377CE70052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
||||||
|
EB0A22E62FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
|
A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||||
EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
||||||
EB5C37FC2E377CE70052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
EB5C37FC2E377CE70052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
||||||
EB1325D62E701D8E00FD749C /* Settings.swift in Sources */,
|
EB1325D62E701D8E00FD749C /* Settings.swift in Sources */,
|
||||||
EBBC93B32EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */,
|
EBBC93B32EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */,
|
||||||
EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */,
|
EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */,
|
||||||
EB5C37FE2E377CE70052EFD7 /* offline_wire_formats.pb.swift in Sources */,
|
EB5C37FE2E377CE70052EFD7 /* offline_wire_formats.pb.swift in Sources */,
|
||||||
EB4F9D1F2EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
|
||||||
EB886E6A2E4F37B90052B7D9 /* ErrorAlertHandler.swift in Sources */,
|
EB886E6A2E4F37B90052B7D9 /* ErrorAlertHandler.swift in Sources */,
|
||||||
EB886E5E2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
EB886E5E2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
||||||
EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
|
EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
|
||||||
@@ -1463,7 +1557,6 @@
|
|||||||
EBBEE1462EC9D93400AA7831 /* PhotoManager.swift in Sources */,
|
EBBEE1462EC9D93400AA7831 /* PhotoManager.swift in Sources */,
|
||||||
EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */,
|
EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */,
|
||||||
EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */,
|
EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */,
|
||||||
EB979F9A2E50CAA6008B8C5C /* PieProgressView.swift in Sources */,
|
|
||||||
EB979F972E50CA23008B8C5C /* SendModel.swift in Sources */,
|
EB979F972E50CA23008B8C5C /* SendModel.swift in Sources */,
|
||||||
EB979F982E50CA23008B8C5C /* QuickDropiOSApp.swift in Sources */,
|
EB979F982E50CA23008B8C5C /* QuickDropiOSApp.swift in Sources */,
|
||||||
EB16B61A2E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
EB16B61A2E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
||||||
@@ -1488,14 +1581,16 @@
|
|||||||
EB950B4A2E520436002D68FA /* securegcm.pb.swift in Sources */,
|
EB950B4A2E520436002D68FA /* securegcm.pb.swift in Sources */,
|
||||||
EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */,
|
EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */,
|
||||||
EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */,
|
EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */,
|
||||||
|
A1B2C3D92F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||||
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||||
EB4F9D212EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
|
||||||
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
|
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
|
||||||
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
|
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
|
||||||
|
EB0A22E32FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
EB950B582E5205B4002D68FA /* Zip.swift in Sources */,
|
EB950B582E5205B4002D68FA /* Zip.swift in Sources */,
|
||||||
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */,
|
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */,
|
||||||
EBA1BB1F2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */,
|
EBA1BB1F2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */,
|
||||||
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */,
|
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB950B522E520436002D68FB /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */,
|
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */,
|
||||||
EBBC93B22EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */,
|
EBBC93B22EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */,
|
||||||
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */,
|
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */,
|
||||||
@@ -1510,7 +1605,6 @@
|
|||||||
EB950B3F2E52038A002D68FA /* FilePicker.swift in Sources */,
|
EB950B3F2E52038A002D68FA /* FilePicker.swift in Sources */,
|
||||||
EB4F9D252EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */,
|
EB4F9D252EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */,
|
||||||
EB950B402E52038A002D68FA /* Configuration.swift in Sources */,
|
EB950B402E52038A002D68FA /* Configuration.swift in Sources */,
|
||||||
EB950B412E52038A002D68FA /* PieProgressView.swift in Sources */,
|
|
||||||
EB950B592E520649002D68FA /* AudioManager.swift in Sources */,
|
EB950B592E520649002D68FA /* AudioManager.swift in Sources */,
|
||||||
EB950B752E52111C002D68FA /* Attachments.swift in Sources */,
|
EB950B752E52111C002D68FA /* Attachments.swift in Sources */,
|
||||||
EB950B6F2E5206E2002D68FA /* ShareViewController.swift in Sources */,
|
EB950B6F2E5206E2002D68FA /* ShareViewController.swift in Sources */,
|
||||||
@@ -1579,7 +1673,7 @@
|
|||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
@@ -1631,7 +1725,7 @@
|
|||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
@@ -1652,7 +1746,6 @@
|
|||||||
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
|
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = EXTENSION;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = EXTENSION;
|
||||||
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = "";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
@@ -1719,8 +1812,8 @@
|
|||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SHARED_BUILD = 107;
|
SHARED_BUILD = 149;
|
||||||
SHARED_VERSION = 2.10.3;
|
SHARED_VERSION = 4.0;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -1782,8 +1875,8 @@
|
|||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SHARED_BUILD = 107;
|
SHARED_BUILD = 149;
|
||||||
SHARED_VERSION = 2.10.3;
|
SHARED_VERSION = 4.0;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -1826,7 +1919,7 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSUIElement = YES;
|
INFOPLIST_KEY_LSUIElement = YES;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
||||||
@@ -1891,7 +1984,7 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSUIElement = YES;
|
INFOPLIST_KEY_LSUIElement = YES;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
||||||
@@ -1955,7 +2048,7 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSUIElement = YES;
|
INFOPLIST_KEY_LSUIElement = YES;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
||||||
@@ -2021,7 +2114,7 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSUIElement = YES;
|
INFOPLIST_KEY_LSUIElement = YES;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
||||||
@@ -2077,7 +2170,7 @@
|
|||||||
INFOPLIST_FILE = "QuickDropShareExtension copy-Info.plist";
|
INFOPLIST_FILE = "QuickDropShareExtension copy-Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
@@ -2129,7 +2222,7 @@
|
|||||||
INFOPLIST_FILE = "QuickDropShareExtension copy-Info.plist";
|
INFOPLIST_FILE = "QuickDropShareExtension copy-Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
@@ -2171,7 +2264,7 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
@@ -2216,7 +2309,7 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
@@ -2256,7 +2349,7 @@
|
|||||||
INFOPLIST_FILE = iOSShareExtension/Info.plist;
|
INFOPLIST_FILE = iOSShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = iOSShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = iOSShareExtension;
|
||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -2287,7 +2380,7 @@
|
|||||||
INFOPLIST_FILE = iOSShareExtension/Info.plist;
|
INFOPLIST_FILE = iOSShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = iOSShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = iOSShareExtension;
|
||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/leif-ibsen/BigInt",
|
"location" : "https://github.com/leif-ibsen/BigInt",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "8c6f93aa37504b7b1ba3954335b5548a19fbbd82",
|
"revision" : "c2f49bee454760e433bafff17bfdc5b96f75d7c8",
|
||||||
"version" : "1.22.0"
|
"version" : "1.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -42,8 +42,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/devicekit/DeviceKit",
|
"location" : "https://github.com/devicekit/DeviceKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "581df61650bc457ec00373a592a84be3e7468eb1",
|
"revision" : "56b997e8a61707218f9af09f32b2a1d1806fd792",
|
||||||
"version" : "5.7.0"
|
"version" : "5.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"location" : "https://github.com/leonboe1/LUI.git",
|
"location" : "https://github.com/leonboe1/LUI.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "19b90ffd9778db54391501f0ba1319e0a1c12b1f"
|
"revision" : "3af5f2e26d102887980957b2d88a38ea8604b8d8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -87,8 +87,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-protobuf.git",
|
"location" : "https://github.com/apple/swift-protobuf.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "c169a5744230951031770e27e475ff6eefe51f9d",
|
"revision" : "f6506eaa86ed2e01cb0ae14a75035b7fdbf0918f",
|
||||||
"version" : "1.33.3"
|
"version" : "1.38.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
"location" : "https://github.com/siteline/swiftui-introspect",
|
"location" : "https://github.com/siteline/swiftui-introspect",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "b10fc8a284abb9bd908d77243d2ba3b3d8ea6ebc"
|
"revision" : "c3db5696c625f8571f5f26fb292568db517374cc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
language = "de"
|
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
@@ -51,6 +50,9 @@
|
|||||||
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<StoreKitConfigurationFileReference
|
||||||
|
identifier = "../../ConfigSynced.storekit">
|
||||||
|
</StoreKitConfigurationFileReference>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = ""
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
|||||||
@@ -20,14 +20,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
private var statusItem: NSStatusItem?
|
private var statusItem: NSStatusItem?
|
||||||
|
|
||||||
var welcomeWindow: NSWindow?
|
var welcomeWindow: NSWindow?
|
||||||
var introductionWindow: NSWindow?
|
|
||||||
var plusWindow: NSWindow?
|
var plusWindow: NSWindow?
|
||||||
|
|
||||||
private var sheetView: NSPanel? = nil
|
private var sheetView: NSPanel? = nil
|
||||||
private var sheetAttachedWindow: NSWindow? = nil
|
private var sheetAttachedWindow: NSWindow? = nil
|
||||||
private var errorAlertHandler = ErrorAlertHandler.shared
|
private var errorAlertHandler = ErrorAlertHandler.shared
|
||||||
|
|
||||||
private var iapManager: IAPManager?
|
|
||||||
private var receiveModel: ReceiveModel?
|
private var receiveModel: ReceiveModel?
|
||||||
|
|
||||||
var showsFirewallAlert = false
|
var showsFirewallAlert = false
|
||||||
@@ -36,12 +34,41 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
|
|
||||||
// MARK: NSApplicationDelegate functions
|
// MARK: NSApplicationDelegate functions
|
||||||
|
|
||||||
func applicationWillFinishLaunching(_: Notification) {
|
|
||||||
LUIInit(configuration: configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_: Notification) {
|
func applicationDidFinishLaunching(_: Notification) {
|
||||||
|
UNUserNotificationCenter.current().delegate = self
|
||||||
|
|
||||||
|
// app did not lauch before
|
||||||
|
if !Settings.sharedInstance.appLaunchedBefore {
|
||||||
|
runAfter(seconds: 0.1) { log("[AppDelegate] Opening Welcome Screen") }
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
// open welcome screen in next cycle (await LUIInit)
|
||||||
|
self.openMainWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// user installed the app after the IAP was implemented, set the user as eligible for IAP
|
||||||
|
Settings.sharedInstance.isEligibleForIap = true
|
||||||
|
LUIInit(configuration: configuration)
|
||||||
|
} else {
|
||||||
|
// app launched before
|
||||||
|
#if GITHUB
|
||||||
|
runAfter(seconds: 0.1) { log("[AppDelegate] Downloaded from GitHub.") }
|
||||||
|
#else
|
||||||
|
// user installed the app before the IAP was implemented, grant the plus version
|
||||||
|
if !Settings.sharedInstance.isEligibleForIap {
|
||||||
|
runAfter(seconds: 0.1) { log("[AppDelegate] Granting QuickDrop+ for old user") }
|
||||||
|
UserDefaults.standard.set(true, forKey: Settings.UserDefaultsKeys.plusVersionLegacy.rawValue)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
BezelNotification.show(messageText: "ReadyToReceive".localized(), icon: .receiveIcon)
|
||||||
|
|
||||||
|
// only start receiving immediately for existing user, for new users we want to delay the permission prompt
|
||||||
|
LUIInit(configuration: configuration)
|
||||||
|
startReceiving()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Menu Bar
|
||||||
|
|
||||||
let menu = NSMenu()
|
let menu = NSMenu()
|
||||||
|
|
||||||
@@ -61,7 +88,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
|
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
let userManualItem = NSMenuItem(title: "UserManual".localized(), action: #selector(openApplicableScreen), keyEquivalent: "")
|
let userManualItem = NSMenuItem(title: "UserManual".localized(), action: #selector(openMainWindow), keyEquivalent: "")
|
||||||
userManualItem.image = NSImage(systemSymbolName: "gear", accessibilityDescription: nil)
|
userManualItem.image = NSImage(systemSymbolName: "gear", accessibilityDescription: nil)
|
||||||
menu.addItem(userManualItem)
|
menu.addItem(userManualItem)
|
||||||
|
|
||||||
@@ -73,45 +100,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
statusItem?.button?.image = NSImage(named: "MenuBarIcon")
|
statusItem?.button?.image = NSImage(named: "MenuBarIcon")
|
||||||
statusItem?.menu = menu
|
statusItem?.menu = menu
|
||||||
statusItem?.behavior = .removalAllowed
|
statusItem?.behavior = .removalAllowed
|
||||||
|
|
||||||
iapManager = IAPManager.sharedInstance
|
|
||||||
|
// MARK: Nearby Connections Callbacks
|
||||||
// app did not lauch before
|
|
||||||
if !Settings.shared.appLaunchedBefore {
|
|
||||||
log("[AppDelegate] Opening Welcome Screen")
|
|
||||||
// open welcome screen
|
|
||||||
openIntroductionWindow()
|
|
||||||
|
|
||||||
// user installed the app after the IAP was implemented, set the user as eligible for IAP
|
|
||||||
Settings.shared.isEligibleForIap = true
|
|
||||||
} else {
|
|
||||||
// app launched before
|
|
||||||
|
|
||||||
#if GITHUB
|
|
||||||
log("[AppDelegate] Downloaded from GitHub.")
|
|
||||||
#else
|
|
||||||
// user installed the app before the IAP was implemented, grant the plus version
|
|
||||||
if !Settings.shared.isEligibleForIap {
|
|
||||||
log("[AppDelegate] Granting QuickDrop+ for old user")
|
|
||||||
Settings.shared.gotPlus = true
|
|
||||||
UserDefaults.standard.set(true, forKey: Settings.UserDefaultsKeys.plusVersionLegacy.rawValue)
|
|
||||||
} else {
|
|
||||||
if !fullVersion() {
|
|
||||||
log("[AppDelegate] New user - QuickDrop+ not available")
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
log("[AppDelegate] QuickDrop+ available")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
BezelNotification.show(messageText: "ReadyToReceive".localized(), icon: .receiveIcon)
|
|
||||||
|
|
||||||
// only start receiving immediately for existing user, for new users we want to delay the permission prompt
|
|
||||||
startReceiving()
|
|
||||||
}
|
|
||||||
|
|
||||||
UNUserNotificationCenter.current().delegate = self
|
|
||||||
|
|
||||||
NearbyConnectionManager.shared.connectionUpdateCallback = { isConnected in
|
NearbyConnectionManager.shared.connectionUpdateCallback = { isConnected in
|
||||||
if isConnected {
|
if isConnected {
|
||||||
@@ -128,11 +119,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
self.visibleItem?.title = self.getDefaultVisibleLabel()
|
self.visibleItem?.title = self.getDefaultVisibleLabel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Logging
|
||||||
|
|
||||||
log("[AppDelegate] Application did finish launching")
|
log("[AppDelegate] Application did finish launching")
|
||||||
|
log("[AppDelegate] Currently running apps: \(NSWorkspace.shared.runningApplications.map { $0.localizedName ?? "-" })")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func startReceiving() {
|
func startReceiving() {
|
||||||
|
guard receiveModel == nil else {
|
||||||
|
log("[AppDelegate] startReceiving called while receiver is already active. Ignoring duplicate call.")
|
||||||
|
ClipboardSyncCoordinator.shared.start()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
receiveModel = ReceiveModel(controlPlusScreen: { shouldOpen in
|
receiveModel = ReceiveModel(controlPlusScreen: { shouldOpen in
|
||||||
if shouldOpen {
|
if shouldOpen {
|
||||||
self.openPlusScreen()
|
self.openPlusScreen()
|
||||||
@@ -145,12 +146,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ClipboardSyncCoordinator.shared.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func windowWillClose(_ notification: Notification) {
|
func windowWillClose(_ notification: Notification) {
|
||||||
if let window = notification.object as? NSWindow, window == welcomeWindow {
|
if let window = notification.object as? NSWindow, window == welcomeWindow {
|
||||||
log("Welcome window closed")
|
log("[AppDelegate] Welcome window closed")
|
||||||
welcomeWindow = nil
|
welcomeWindow = nil
|
||||||
NSApp.setActivationPolicy(.accessory)
|
NSApp.setActivationPolicy(.accessory)
|
||||||
}
|
}
|
||||||
@@ -158,47 +161,62 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
|
|
||||||
|
|
||||||
func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool {
|
func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool {
|
||||||
self.openApplicableScreen()
|
self.openMainWindow()
|
||||||
statusItem?.isVisible = true
|
statusItem?.isVisible = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@objc func openApplicableScreen() {
|
|
||||||
if Settings.shared.appLaunchedBefore {
|
|
||||||
openMainWindow()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
openIntroductionWindow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public func userNotificationCenter(_: UNUserNotificationCenter,
|
public func userNotificationCenter(_: UNUserNotificationCenter,
|
||||||
willPresent _: UNNotification,
|
willPresent notification: UNNotification,
|
||||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
|
if ClipboardSyncNotificationPresenter.isClipboardSyncNotification(notification) {
|
||||||
|
completionHandler([.banner])
|
||||||
|
return
|
||||||
|
}
|
||||||
completionHandler([.sound, .banner])
|
completionHandler([.sound, .banner])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func applicationWillTerminate(_: Notification) {
|
func applicationWillTerminate(_: Notification) {
|
||||||
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
|
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
|
||||||
|
ClipboardSyncCoordinator.shared.stop()
|
||||||
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||||
|
NearbyConnectionManager.shared.becomeInvisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func application(_ application: NSApplication, open urls: [URL]) {
|
func application(_ application: NSApplication, open urls: [URL]) {
|
||||||
|
|
||||||
|
log("[AppDelegate] application(open:): Received URLs: \(urls)")
|
||||||
|
|
||||||
for url in urls {
|
for url in urls {
|
||||||
if url.scheme == "quickdrop" {
|
if url.scheme == "quickdrop" {
|
||||||
|
|
||||||
switch url.host {
|
switch url.host {
|
||||||
case "sendLog":
|
case "sendLog":
|
||||||
sendLoggingString()
|
// If we're in an extension, redirect to the main app
|
||||||
|
if Bundle.main.bundlePath.hasSuffix(".appex") {
|
||||||
|
log("[AppDelegate] sendLoggingString: in extension, redirecting to main app")
|
||||||
|
|
||||||
|
if let url = URL(string: "quickdrop://sendLog") {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
LogExportPresenter.showUploadLogsAlert()
|
||||||
case "openLog":
|
case "openLog":
|
||||||
if let url = LogManager.sharedInstance.logFileURL {
|
if let url = LogManager.sharedInstance.logFileURL {
|
||||||
|
log("[AppDelegate] Opening log file: \(url)")
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([url])
|
NSWorkspace.shared.activateFileViewerSelecting([url])
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
log("[AppDelegate] No log file found to open.")
|
||||||
|
}
|
||||||
|
case "mockTransferFailure":
|
||||||
|
ErrorAlertHandler.shared.showErrorAlert(for: "Test Device", error: NearbyError.inputOutput)
|
||||||
case "removeData":
|
case "removeData":
|
||||||
Settings.shared.deleteAllUserDefaults()
|
Settings.sharedInstance.deleteAllUserDefaults()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -231,6 +249,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
|
|
||||||
|
|
||||||
@objc func openMainWindow() {
|
@objc func openMainWindow() {
|
||||||
|
defer {
|
||||||
|
if let welcomeWindow, !Settings.sharedInstance.appLaunchedBefore {
|
||||||
|
runAfter(seconds: 0.2) {
|
||||||
|
IntroductionSheetManager.sharedInstance.openIntroductionWindow(on: welcomeWindow, startReceiving: self.startReceiving) {
|
||||||
|
self.startReceiving()
|
||||||
|
Settings.sharedInstance.appLaunchedBefore = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NSApp.setActivationPolicy(.regular)
|
NSApp.setActivationPolicy(.regular)
|
||||||
|
|
||||||
@@ -271,49 +299,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
welcomeWindow?.makeKeyAndOrderFront(nil)
|
welcomeWindow?.makeKeyAndOrderFront(nil)
|
||||||
welcomeWindow?.level = .normal
|
welcomeWindow?.level = .normal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@objc func openIntroductionWindow() {
|
|
||||||
|
|
||||||
NSApp.setActivationPolicy(.regular)
|
|
||||||
|
|
||||||
if let window = introductionWindow {
|
|
||||||
if window.isMiniaturized {
|
|
||||||
window.deminiaturize(nil)
|
|
||||||
}
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
|
||||||
window.makeKeyAndOrderFront(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let introductionView = IntroductionView(startReceiving: startReceiving) {
|
|
||||||
Settings.shared.appLaunchedBefore = true
|
|
||||||
self.introductionWindow?.close()
|
|
||||||
self.introductionWindow = nil
|
|
||||||
self.openMainWindow()
|
|
||||||
}
|
|
||||||
|
|
||||||
introductionWindow = NSWindow(
|
|
||||||
contentRect: NSRect(x: 0, y: 0, width: IntroductionView.width, height: IntroductionView.height),
|
|
||||||
styleMask: [.titled, .closable],
|
|
||||||
backing: .buffered,
|
|
||||||
defer: false
|
|
||||||
)
|
|
||||||
|
|
||||||
introductionWindow?.styleMask.insert(.fullSizeContentView)
|
|
||||||
introductionWindow?.titleVisibility = .hidden
|
|
||||||
introductionWindow?.titlebarAppearsTransparent = true
|
|
||||||
|
|
||||||
introductionWindow?.center()
|
|
||||||
introductionWindow?.isReleasedWhenClosed = false
|
|
||||||
introductionWindow?.setFrameAutosaveName("IntroductionScreen")
|
|
||||||
introductionWindow?.contentView = NSHostingView(rootView: introductionView)
|
|
||||||
introductionWindow?.delegate = self
|
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
|
||||||
introductionWindow?.makeKeyAndOrderFront(nil)
|
|
||||||
introductionWindow?.level = .normal
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@objc func openPlusScreen() {
|
@objc func openPlusScreen() {
|
||||||
@@ -321,11 +306,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
if let window = plusWindow, window.isVisible {
|
if let window = plusWindow, window.isVisible {
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
log("Plus screen already open, bringing to front")
|
log("[AppDelegate] Plus screen already open, bringing to front")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Opening Plus screen")
|
log("[AppDelegate] Opening Plus screen")
|
||||||
|
|
||||||
// Create the welcome screen SwiftUI view
|
// Create the welcome screen SwiftUI view
|
||||||
let plusView = GetPlusViewV4(showSheet: Binding(get: { self.plusWindow != nil }, set: { newValue in
|
let plusView = GetPlusViewV4(showSheet: Binding(get: { self.plusWindow != nil }, set: { newValue in
|
||||||
@@ -335,7 +320,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
}
|
}
|
||||||
}), hasSheet: false)
|
}), hasSheet: false)
|
||||||
|
|
||||||
let plusViewWidth: CGFloat = 550
|
let plusViewWidth: CGFloat = 750
|
||||||
let plusViewHeight: CGFloat = 750
|
let plusViewHeight: CGFloat = 750
|
||||||
|
|
||||||
// Create an NSWindow to host the SwiftUI view
|
// Create an NSWindow to host the SwiftUI view
|
||||||
@@ -365,7 +350,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
// Ensure the window is always on top
|
// Ensure the window is always on top
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
window.level = .normal
|
window.level = .floating
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -410,10 +395,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
let scanner = DeviceToDeviceHeuristicScanner.shared
|
let scanner = DeviceToDeviceHeuristicScanner.shared
|
||||||
scanner.scan { allowed in
|
scanner.scan { allowed in
|
||||||
if allowed {
|
if allowed {
|
||||||
log("✅ Device-to-device likely allowed (peer responded on LAN).")
|
log("[AppDelegate] ✅ Device-to-device likely allowed (peer responded on LAN).")
|
||||||
ErrorAlertHandler.shared.closeApIsolationAlert()
|
ErrorAlertHandler.shared.closeApIsolationAlert()
|
||||||
} else {
|
} else {
|
||||||
log("❌ Device-to-device check failed. Informing user...")
|
log("[AppDelegate] ❌ Device-to-device check failed. Informing user...")
|
||||||
ErrorAlertHandler.shared.openAlert(type: .ApIsolation)
|
ErrorAlertHandler.shared.openAlert(type: .ApIsolation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 618 KiB After Width: | Height: | Size: 585 KiB |
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 672 KiB |
@@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// Configuration.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 15.08.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import LUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
let configuration = LUIConfiguration(
|
||||||
|
appName: "QuickDrop",
|
||||||
|
|
||||||
|
plusVersionInfo: GetPlusViewInformation(
|
||||||
|
lifetimeID: "plusversion",
|
||||||
|
monthlyID: "quickdrop.monthly",
|
||||||
|
yearlyID: "quickdrop.yearly",
|
||||||
|
plusViewHeader: isMac() ? "plusview_title" : nil,
|
||||||
|
description: isMac() ? "plusview_description" : "plusview_description_phone",
|
||||||
|
onBoughtPlusVersionAlertMessage: "plusview_success_description",
|
||||||
|
plusVersionFeatures: [],
|
||||||
|
settingsLabelColor: .orange,
|
||||||
|
settingsFooter: "plusview_footer_phone"
|
||||||
|
),
|
||||||
|
|
||||||
|
hasRoundedFont: false,
|
||||||
|
|
||||||
|
switchedToInAppPurchaseWithBundleVersion: AppVersionBundle(appVersion: "1.2", buildVersion: 0, legacyUserDefaultsKey: Settings.UserDefaultsKeys.plusVersionLegacy.rawValue),
|
||||||
|
|
||||||
|
copyrightInformation: licenseText,
|
||||||
|
|
||||||
|
logUploadDisclaimerMessage: "log_upload_disclaimer_message",
|
||||||
|
|
||||||
|
loggingUrl: AppGroup.appGroupDirectory,
|
||||||
|
|
||||||
|
introductionViewInformation: "QuickDropWelcomeDescription",
|
||||||
|
|
||||||
|
introductionViewImage: Image(.quickDropMockup),
|
||||||
|
|
||||||
|
usesAutomaticReviewPromptAndShareSheet: false,
|
||||||
|
|
||||||
|
usesPagedIntroduction: true,
|
||||||
|
|
||||||
|
supportsSplitView: false,
|
||||||
|
|
||||||
|
supportsLiquidGlass: true,
|
||||||
|
|
||||||
|
supportsPortraitSplitView: false,
|
||||||
|
|
||||||
|
supportsDarkModeShadow: false,
|
||||||
|
|
||||||
|
privacyPolicyURL: .privacyPolicyURL
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
static let privacyPolicyURL = "https://leonboettger.com/quickdrop-privacy"
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
"value" : "QuickDrop"
|
"value" : "QuickDrop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
"CFBundleName" : {
|
"CFBundleName" : {
|
||||||
"comment" : "Bundle name",
|
"comment" : "Bundle name",
|
||||||
@@ -23,7 +24,8 @@
|
|||||||
"value" : "QuickDrop"
|
"value" : "QuickDrop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
"NSHumanReadableCopyright" : {
|
"NSHumanReadableCopyright" : {
|
||||||
"comment" : "Copyright (human-readable)",
|
"comment" : "Copyright (human-readable)",
|
||||||
@@ -32,10 +34,11 @@
|
|||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "new",
|
||||||
"value" : ""
|
"value" : "Copyright © 2025-2026 Leon Böttger"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
"NSLocalNetworkUsageDescription" : {
|
"NSLocalNetworkUsageDescription" : {
|
||||||
"comment" : "Privacy - Local Network Usage Description",
|
"comment" : "Privacy - Local Network Usage Description",
|
||||||
|
|||||||
@@ -6,5 +6,9 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>group.com.leonboettger.neardrop</string>
|
<string>group.com.leonboettger.neardrop</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -5,25 +5,18 @@
|
|||||||
// Created by Leon Böttger on 09.09.25.
|
// Created by Leon Böttger on 09.09.25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import LUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
class Settings: ObservableObject {
|
final class Settings: SharedInstance {
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
static var shared = Settings()
|
static var sharedInstance = Settings()
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Published UserDefaults Backed Properties
|
// MARK: - Published UserDefaults Backed Properties
|
||||||
|
|
||||||
#if GITHUB
|
|
||||||
let gotPlus = true
|
|
||||||
#else
|
|
||||||
@Published var gotPlus = UserDefaults.standard.bool(forKey: UserDefaultsKeys.plusVersion.rawValue)
|
|
||||||
{ didSet { UserDefaults.standard.set(gotPlus, forKey: UserDefaultsKeys.plusVersion.rawValue) }}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
@Published var automaticallyAcceptFiles: Bool = UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue)
|
@Published var automaticallyAcceptFiles: Bool = UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue)
|
||||||
{ didSet { UserDefaults.standard.set(automaticallyAcceptFiles, forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue) }}
|
{ didSet { UserDefaults.standard.set(automaticallyAcceptFiles, forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue) }}
|
||||||
|
|
||||||
@@ -41,6 +34,10 @@ class Settings: ObservableObject {
|
|||||||
|
|
||||||
var incomingTransmissionCount: Int = UserDefaults.standard.integer(forKey: UserDefaultsKeys.transmissionCount.rawValue)
|
var incomingTransmissionCount: Int = UserDefaults.standard.integer(forKey: UserDefaultsKeys.transmissionCount.rawValue)
|
||||||
{ didSet { UserDefaults.standard.set(incomingTransmissionCount, forKey: UserDefaultsKeys.transmissionCount.rawValue) }}
|
{ didSet { UserDefaults.standard.set(incomingTransmissionCount, forKey: UserDefaultsKeys.transmissionCount.rawValue) }}
|
||||||
|
|
||||||
|
|
||||||
|
var incomingNotificationCount: Int = UserDefaults.standard.integer(forKey: UserDefaultsKeys.incomingNotificationCount.rawValue)
|
||||||
|
{ didSet { UserDefaults.standard.set(incomingNotificationCount, forKey: UserDefaultsKeys.incomingNotificationCount.rawValue) }}
|
||||||
|
|
||||||
|
|
||||||
var endpointID: String? = UserDefaults.standard.string(forKey: UserDefaultsKeys.endpointID.rawValue)
|
var endpointID: String? = UserDefaults.standard.string(forKey: UserDefaultsKeys.endpointID.rawValue)
|
||||||
@@ -65,18 +62,18 @@ class Settings: ObservableObject {
|
|||||||
}
|
}
|
||||||
defaults.synchronize()
|
defaults.synchronize()
|
||||||
|
|
||||||
Settings.shared = Settings()
|
Settings.sharedInstance = Settings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - UserDefaults Keys
|
// MARK: - UserDefaults Keys
|
||||||
|
|
||||||
public enum UserDefaultsKeys: String, CaseIterable {
|
public enum UserDefaultsKeys: String, CaseIterable, RawRepresentable {
|
||||||
case isEligibleForIap = "isEligibleForIap"
|
case isEligibleForIap = "isEligibleForIap"
|
||||||
case appLaunchedBefore = "ShowedWelcomeScreen"
|
case appLaunchedBefore = "ShowedWelcomeScreen"
|
||||||
case plusVersion = "plusVersion"
|
|
||||||
case transmissionCount = "reviewRequestCountKey"
|
case transmissionCount = "reviewRequestCountKey"
|
||||||
|
case incomingNotificationCount = "incomingNotificationCount"
|
||||||
case automaticallyAcceptFiles = "automaticallyAcceptFiles"
|
case automaticallyAcceptFiles = "automaticallyAcceptFiles"
|
||||||
case saveFolderBookmark = "saveFolderBookmark"
|
case saveFolderBookmark = "saveFolderBookmark"
|
||||||
case openFinderAfterReceiving = "openFinderAfterReceiving"
|
case openFinderAfterReceiving = "openFinderAfterReceiving"
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
//
|
||||||
|
// ClipboardSyncCoordinator.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
|
||||||
|
#if os(macOS) && !EXTENSION
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
final class ClipboardSyncCoordinator {
|
||||||
|
static let shared = ClipboardSyncCoordinator()
|
||||||
|
|
||||||
|
private let pollInterval: TimeInterval = 1
|
||||||
|
private let transferTimeout: TimeInterval = 75
|
||||||
|
|
||||||
|
private var isStarted = false
|
||||||
|
private var pollTimer: Timer?
|
||||||
|
private var trustStoreObserver: NSObjectProtocol?
|
||||||
|
private var lastObservedChangeCount: Int
|
||||||
|
private var suppressedClipboardText: String?
|
||||||
|
private var startedOwnDiscovery = false
|
||||||
|
private var activeTransfersByFingerprint: [String: ClipboardSyncTransferDelegate] = [:]
|
||||||
|
private var queuedTextByFingerprint: [String: String] = [:]
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
lastObservedChangeCount = NSPasteboard.general.changeCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
guard !isStarted else {
|
||||||
|
refreshPollingState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isStarted = true
|
||||||
|
trustStoreObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: TrustStore.trustedCertificatesDidChange,
|
||||||
|
object: TrustStore.shared,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.refreshPollingState()
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshPollingState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
guard isStarted || pollTimer != nil else { return }
|
||||||
|
|
||||||
|
log("[ClipboardSyncCoordinator] Stopping clipboard watcher")
|
||||||
|
isStarted = false
|
||||||
|
if let trustStoreObserver {
|
||||||
|
NotificationCenter.default.removeObserver(trustStoreObserver)
|
||||||
|
self.trustStoreObserver = nil
|
||||||
|
}
|
||||||
|
stopPolling(clearTransfers: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshPollingState() {
|
||||||
|
guard isStarted else { return }
|
||||||
|
|
||||||
|
guard hasTrustedClipboardSyncDevice else {
|
||||||
|
if pollTimer != nil {
|
||||||
|
log("[ClipboardSyncCoordinator] Pausing clipboard watcher because no trusted clipboard sync device is saved")
|
||||||
|
}
|
||||||
|
stopPolling(clearTransfers: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
startPollingIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hasTrustedClipboardSyncDevice: Bool {
|
||||||
|
TrustStore.shared.trustedCertificates.values.contains {
|
||||||
|
$0.grantedUseCases.contains(.clipboardSync)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startPollingIfNeeded() {
|
||||||
|
guard pollTimer == nil else { return }
|
||||||
|
|
||||||
|
let manager = NearbyConnectionManager.shared
|
||||||
|
if !manager.isDeviceDiscoveryActive {
|
||||||
|
startedOwnDiscovery = true
|
||||||
|
manager.startDeviceDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
lastObservedChangeCount = NSPasteboard.general.changeCount
|
||||||
|
log("[ClipboardSyncCoordinator] Starting clipboard watcher at changeCount=\(lastObservedChangeCount)")
|
||||||
|
pollTimer = Timer.scheduledTimer(withTimeInterval: pollInterval, repeats: true) { [weak self] _ in
|
||||||
|
self?.pollClipboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopPolling(clearTransfers: Bool) {
|
||||||
|
pollTimer?.invalidate()
|
||||||
|
pollTimer = nil
|
||||||
|
suppressedClipboardText = nil
|
||||||
|
if clearTransfers {
|
||||||
|
activeTransfersByFingerprint.values.forEach { $0.cancelTimeout() }
|
||||||
|
activeTransfersByFingerprint.removeAll()
|
||||||
|
queuedTextByFingerprint.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard startedOwnDiscovery else { return }
|
||||||
|
startedOwnDiscovery = false
|
||||||
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
func suppressNextClipboardSync(for text: String) {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
setSuppressedClipboardText(text)
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
setSuppressedClipboardText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setSuppressedClipboardText(_ text: String) {
|
||||||
|
log("[ClipboardSyncCoordinator] Suppressing next clipboard sync for incoming text with \(text.utf8.count) bytes")
|
||||||
|
suppressedClipboardText = text
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pollClipboard() {
|
||||||
|
let pasteboard = NSPasteboard.general
|
||||||
|
let currentChangeCount = pasteboard.changeCount
|
||||||
|
guard currentChangeCount != lastObservedChangeCount else { return }
|
||||||
|
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard change detected: \(lastObservedChangeCount) -> \(currentChangeCount)")
|
||||||
|
lastObservedChangeCount = currentChangeCount
|
||||||
|
|
||||||
|
guard let clipboardText = pasteboard.string(forType: .string) else {
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard change ignored because no plain text content is available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard !clipboardText.isEmpty else {
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard change ignored because text content is empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if suppressedClipboardText == clipboardText {
|
||||||
|
log("[ClipboardSyncCoordinator] Ignoring clipboard change because it matches an incoming clipboard transfer")
|
||||||
|
suppressedClipboardText = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
suppressedClipboardText = nil
|
||||||
|
|
||||||
|
sendClipboardText(clipboardText)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendClipboardText(_ clipboardText: String) {
|
||||||
|
let targets = nearbyTrustedClipboardTargets()
|
||||||
|
guard !targets.isEmpty else {
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard change will not be sent because no trusted clipboard-capable devices are nearby")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[ClipboardSyncCoordinator] Sending clipboard contents to \(targets.count) trusted clipboard-capable device(s)")
|
||||||
|
|
||||||
|
for target in targets {
|
||||||
|
startTransfer(to: target, clipboardText: clipboardText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startTransfer(to target: ClipboardSyncTarget, clipboardText: String) {
|
||||||
|
guard activeTransfersByFingerprint[target.fingerprint] == nil else {
|
||||||
|
queuedTextByFingerprint[target.fingerprint] = clipboardText
|
||||||
|
log("[ClipboardSyncCoordinator] Queued latest clipboard text for \(target.logName) because a transfer is already active")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let delegate = ClipboardSyncTransferDelegate(target: target) { [weak self] finishedTarget, finishedDelegate in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.handleTransferCompleted(for: finishedTarget, delegate: finishedDelegate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeTransfersByFingerprint[target.fingerprint] = delegate
|
||||||
|
delegate.startTimeout(after: transferTimeout)
|
||||||
|
|
||||||
|
NearbyConnectionManager.shared.startOutgoingTransfer(
|
||||||
|
deviceID: target.deviceID,
|
||||||
|
delegate: delegate,
|
||||||
|
urls: [],
|
||||||
|
textToSend: clipboardText,
|
||||||
|
isClipboardSyncTransfer: true,
|
||||||
|
receiverAuthenticationPolicy: .trustedReceiver(fingerprint: target.fingerprint)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleTransferCompleted(for target: ClipboardSyncTarget, delegate: ClipboardSyncTransferDelegate) {
|
||||||
|
guard activeTransfersByFingerprint[target.fingerprint] === delegate else {
|
||||||
|
log("[ClipboardSyncCoordinator] Ignoring stale clipboard sync completion for \(target.logName)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTransfersByFingerprint.removeValue(forKey: target.fingerprint)
|
||||||
|
|
||||||
|
guard let queuedText = queuedTextByFingerprint.removeValue(forKey: target.fingerprint) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let refreshedTarget = nearbyTrustedClipboardTargets()
|
||||||
|
.first(where: { $0.fingerprint == target.fingerprint }) else {
|
||||||
|
log("[ClipboardSyncCoordinator] Dropping queued clipboard text for \(target.logName) because the device is no longer an eligible target")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[ClipboardSyncCoordinator] Sending queued latest clipboard text to \(refreshedTarget.logName)")
|
||||||
|
startTransfer(to: refreshedTarget, clipboardText: queuedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nearbyTrustedClipboardTargets() -> [ClipboardSyncTarget] {
|
||||||
|
let trustedFingerprints = Set(
|
||||||
|
TrustStore.shared.trustedCertificates.keys.map {
|
||||||
|
$0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
var seenFingerprints = Set<String>()
|
||||||
|
|
||||||
|
return NearbyConnectionManager.shared.discoveredDevices()
|
||||||
|
.compactMap { device -> ClipboardSyncTarget? in
|
||||||
|
guard let deviceID = device.id,
|
||||||
|
!deviceID.isEmpty,
|
||||||
|
let fingerprint = device.keyFingerprint,
|
||||||
|
trustedFingerprints.contains(fingerprint),
|
||||||
|
TrustStore.shared.hasUseCaseGrant(.clipboardSync, for: fingerprint),
|
||||||
|
device.supportsClipboardReceive,
|
||||||
|
device.type != .computer,
|
||||||
|
seenFingerprints.insert(fingerprint).inserted else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClipboardSyncTarget(
|
||||||
|
device: device,
|
||||||
|
deviceID: deviceID,
|
||||||
|
fingerprint: fingerprint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sorted { lhs, rhs in
|
||||||
|
lhs.logName < rhs.logName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ClipboardSyncTarget {
|
||||||
|
let device: RemoteDeviceInfo
|
||||||
|
let deviceID: String
|
||||||
|
let fingerprint: String
|
||||||
|
|
||||||
|
var logName: String {
|
||||||
|
device.name ?? deviceID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ClipboardSyncTransferDelegate: OutboundAppDelegate {
|
||||||
|
private let target: ClipboardSyncTarget
|
||||||
|
private let onComplete: (ClipboardSyncTarget, ClipboardSyncTransferDelegate) -> Void
|
||||||
|
private var isCompleted = false
|
||||||
|
private var timeoutWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
|
init(target: ClipboardSyncTarget, onComplete: @escaping (ClipboardSyncTarget, ClipboardSyncTransferDelegate) -> Void) {
|
||||||
|
self.target = target
|
||||||
|
self.onComplete = onComplete
|
||||||
|
}
|
||||||
|
|
||||||
|
func startTimeout(after timeout: TimeInterval) {
|
||||||
|
let timeoutWorkItem = DispatchWorkItem { [weak self] in
|
||||||
|
guard let self, !self.isCompleted else { return }
|
||||||
|
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard sync to \(self.target.logName) timed out")
|
||||||
|
NearbyConnectionManager.shared.cancelTransfer(id: self.target.deviceID)
|
||||||
|
self.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.timeoutWorkItem = timeoutWorkItem
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: timeoutWorkItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelTimeout() {
|
||||||
|
timeoutWorkItem?.cancel()
|
||||||
|
timeoutWorkItem = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addDevice(device _: RemoteDeviceInfo) {}
|
||||||
|
|
||||||
|
func removeDevice(id _: String) {}
|
||||||
|
|
||||||
|
func startTransferWithQrCode(device _: RemoteDeviceInfo) {}
|
||||||
|
|
||||||
|
func connectionWasEstablished(pinCode _: String) {}
|
||||||
|
|
||||||
|
func transferAccepted() {}
|
||||||
|
|
||||||
|
func transferProgress(progress _: Double) {}
|
||||||
|
|
||||||
|
func connectionFailed(error: Error) {
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard sync to \(target.logName) failed: \(error.localizedDescription)")
|
||||||
|
complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferFinished() {
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard sync to \(target.logName) finished")
|
||||||
|
complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func complete() {
|
||||||
|
guard !isCompleted else { return }
|
||||||
|
isCompleted = true
|
||||||
|
cancelTimeout()
|
||||||
|
onComplete(target, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
//
|
||||||
|
// ClipboardSyncNotificationPresenter.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
|
||||||
|
#if os(macOS) && !EXTENSION
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UserNotifications
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
final class ClipboardSyncNotificationPresenter {
|
||||||
|
|
||||||
|
static let shared = ClipboardSyncNotificationPresenter()
|
||||||
|
static let notificationIdentifier = "quickdrop-clipboard-sync"
|
||||||
|
private static let notificationIdentifierPrefix = "\(notificationIdentifier)-"
|
||||||
|
|
||||||
|
private let center = UNUserNotificationCenter.current()
|
||||||
|
private let queue = DispatchQueue(label: "ClipboardSyncNotificationPresenter")
|
||||||
|
private var pendingNotifications: [(String, String?)] = []
|
||||||
|
private var authorizationRequestInFlight = false
|
||||||
|
private var postedNotificationIdentifiers: Set<String> = []
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
static func isClipboardSyncNotification(_ notification: UNNotification) -> Bool {
|
||||||
|
notification.request.identifier == notificationIdentifier
|
||||||
|
|| notification.request.identifier.hasPrefix(notificationIdentifierPrefix)
|
||||||
|
|| notification.request.content.threadIdentifier == notificationIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func present(clipboardText: String, senderDeviceName: String?) {
|
||||||
|
queue.async {
|
||||||
|
self.postNotificationIfAuthorized(
|
||||||
|
clipboardText: clipboardText,
|
||||||
|
senderDeviceName: senderDeviceName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func postNotificationIfAuthorized(clipboardText: String, senderDeviceName: String?) {
|
||||||
|
center.getNotificationSettings { settings in
|
||||||
|
self.queue.async {
|
||||||
|
self.handleAuthorizationStatus(
|
||||||
|
settings.authorizationStatus,
|
||||||
|
clipboardText: clipboardText,
|
||||||
|
senderDeviceName: senderDeviceName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleAuthorizationStatus(
|
||||||
|
_ status: UNAuthorizationStatus,
|
||||||
|
clipboardText: String,
|
||||||
|
senderDeviceName: String?
|
||||||
|
) {
|
||||||
|
switch status {
|
||||||
|
case .authorized, .provisional, .ephemeral:
|
||||||
|
post(clipboardText: clipboardText, senderDeviceName: senderDeviceName)
|
||||||
|
case .notDetermined:
|
||||||
|
pendingNotifications.append((clipboardText, senderDeviceName))
|
||||||
|
requestAuthorizationIfNeeded()
|
||||||
|
case .denied:
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Notifications are denied in system settings; cannot show clipboard sync notification.")
|
||||||
|
@unknown default:
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Unknown notification authorization status; skipping clipboard sync notification.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestAuthorizationIfNeeded() {
|
||||||
|
if authorizationRequestInFlight {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizationRequestInFlight = true
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.center.requestAuthorization(options: [.alert, .badge]) { granted, error in
|
||||||
|
self.queue.async {
|
||||||
|
self.authorizationRequestInFlight = false
|
||||||
|
|
||||||
|
if let error = error {
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Authorization request failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard granted else {
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Authorization request not granted; dropping queued clipboard sync notifications.")
|
||||||
|
self.pendingNotifications.removeAll()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let queued = self.pendingNotifications
|
||||||
|
self.pendingNotifications.removeAll()
|
||||||
|
for (queuedText, queuedSender) in queued {
|
||||||
|
self.post(clipboardText: queuedText, senderDeviceName: queuedSender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func post(clipboardText: String, senderDeviceName: String?) {
|
||||||
|
let senderName = senderDeviceName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "CopiedToClipboard".localized()
|
||||||
|
content.body = preview(for: clipboardText)
|
||||||
|
content.subtitle = senderName?.isEmpty == false ? senderName! : "QuickDrop"
|
||||||
|
content.threadIdentifier = Self.notificationIdentifier
|
||||||
|
content.interruptionLevel = .active
|
||||||
|
|
||||||
|
let identifier = Self.makeNotificationIdentifier()
|
||||||
|
var identifiersToRemove = postedNotificationIdentifiers
|
||||||
|
identifiersToRemove.insert(Self.notificationIdentifier)
|
||||||
|
postedNotificationIdentifiers = [identifier]
|
||||||
|
center.removePendingNotificationRequests(withIdentifiers: Array(identifiersToRemove))
|
||||||
|
center.removeDeliveredNotifications(withIdentifiers: Array(identifiersToRemove))
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
|
||||||
|
center.add(request) { error in
|
||||||
|
if let error = error {
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Could not post notification: \(error.localizedDescription)")
|
||||||
|
} else {
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Posted clipboard sync notification (\(identifier)).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func preview(for clipboardText: String) -> String {
|
||||||
|
clipboardText
|
||||||
|
.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeNotificationIdentifier() -> String {
|
||||||
|
"\(notificationIdentifierPrefix)\(UUID().uuidString)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -36,7 +36,6 @@ class ErrorAlertHandler {
|
|||||||
|
|
||||||
var description = ""
|
var description = ""
|
||||||
let fixInstructions = " " + "Error.FixInstructions".localized()
|
let fixInstructions = " " + "Error.FixInstructions".localized()
|
||||||
|
|
||||||
if let ne = (error as? NearbyError) {
|
if let ne = (error as? NearbyError) {
|
||||||
switch ne {
|
switch ne {
|
||||||
case .inputOutput:
|
case .inputOutput:
|
||||||
@@ -61,6 +60,8 @@ class ErrorAlertHandler {
|
|||||||
description = errorMessage + fixInstructions
|
description = errorMessage + fixInstructions
|
||||||
case .ukey2:
|
case .ukey2:
|
||||||
description = "Error.Crypto".localized() + ": \(ne.localizedDescription)" + fixInstructions
|
description = "Error.Crypto".localized() + ": \(ne.localizedDescription)" + fixInstructions
|
||||||
|
case .notificationSyncNotTrusted:
|
||||||
|
description = "NotificationSyncNotTrustedMessage".localized()
|
||||||
case .canceled(reason: let reason):
|
case .canceled(reason: let reason):
|
||||||
if reason == .timedOut {
|
if reason == .timedOut {
|
||||||
description = reason.localizedDescription() + fixInstructions
|
description = reason.localizedDescription() + fixInstructions
|
||||||
@@ -83,7 +84,7 @@ class ErrorAlertHandler {
|
|||||||
|
|
||||||
let title = String(format: "TransferError".localized(), arguments: [deviceName])
|
let title = String(format: "TransferError".localized(), arguments: [deviceName])
|
||||||
log("Showing alert with title: \"\(title)\" and description: \"\(description)\"")
|
log("Showing alert with title: \"\(title)\" and description: \"\(description)\"")
|
||||||
log("Unsuccessful transmission. Already successful transmissions: \(Settings.shared.incomingTransmissionCount)")
|
log("Unsuccessful transmission. Already successful transmissions: \(Settings.sharedInstance.incomingTransmissionCount)")
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|
||||||
@@ -102,8 +103,13 @@ class ErrorAlertHandler {
|
|||||||
self.isAlertShown = false
|
self.isAlertShown = false
|
||||||
|
|
||||||
if result == .alertFirstButtonReturn {
|
if result == .alertFirstButtonReturn {
|
||||||
//log("Sending logging string")
|
if Bundle.main.bundlePath.hasSuffix(".appex") {
|
||||||
//sendLoggingString()
|
if let url = URL(string: "quickdrop://sendLog") {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LogExportPresenter.showUploadLogsAlert(openSupportMailAfterUpload: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
showAlert(title: title, message: description)
|
showAlert(title: title, message: description)
|
||||||
@@ -161,75 +167,6 @@ class ErrorAlertHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
func sendLoggingString() {
|
|
||||||
// If we're in an extension, redirect to the main app
|
|
||||||
if Bundle.main.bundlePath.hasSuffix(".appex") {
|
|
||||||
log("sendLoggingString: in extension, redirecting to main app")
|
|
||||||
|
|
||||||
if let url = URL(string: "quickdrop://sendLog") {
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're in the main app – do the actual sending
|
|
||||||
if let url = LogManager.sharedInstance.logFileURL {
|
|
||||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
|
|
||||||
log("Sending logging string with file URL: \(url)")
|
|
||||||
log("Currently running apps: \(NSWorkspace.shared.runningApplications.map { $0.localizedName ?? "Unknown" })")
|
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
|
|
||||||
sendEmailWithAttachment(
|
|
||||||
fileURL: url,
|
|
||||||
recipients: ["quickdrop@leonboettger.com"],
|
|
||||||
subject: "QuickDrop \(appVersion) - \(getDeviceAndSystem())"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fileprivate func getDeviceAndSystem() -> String {
|
|
||||||
// Get Mac model identifier
|
|
||||||
var size: Int = 0
|
|
||||||
sysctlbyname("hw.model", nil, &size, nil, 0)
|
|
||||||
|
|
||||||
var modelBuffer = [CChar](repeating: 0, count: size)
|
|
||||||
sysctlbyname("hw.model", &modelBuffer, &size, nil, 0)
|
|
||||||
|
|
||||||
let model = String(cString: modelBuffer)
|
|
||||||
|
|
||||||
// Get macOS version
|
|
||||||
let version = ProcessInfo.processInfo.operatingSystemVersion
|
|
||||||
let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
|
|
||||||
|
|
||||||
return "\(model) - \(versionString)"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func sendEmailWithAttachment(fileURL: URL, recipients: [String], subject: String) {
|
|
||||||
guard let emailService = NSSharingService(named: .composeEmail) else {
|
|
||||||
log("No email service available")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
emailService.recipients = recipients
|
|
||||||
emailService.subject = subject
|
|
||||||
emailService.perform(withItems: [fileURL])
|
|
||||||
|
|
||||||
log("Email service opened with recipients: \(recipients) and subject: \(subject)")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func copyToClipboard(_ text: String) {
|
|
||||||
let pasteboard = NSPasteboard.general
|
|
||||||
pasteboard.clearContents()
|
|
||||||
pasteboard.setString(text, forType: .string)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
enum AlertType: String {
|
enum AlertType: String {
|
||||||
case ApIsolation
|
case ApIsolation
|
||||||
case NetworkFilter
|
case NetworkFilter
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ func sendToSharingService(items: [Any]) {
|
|||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.alertStyle = .critical
|
alert.alertStyle = .critical
|
||||||
alert.messageText = "ShareExtensionDisabledTitle".localized()
|
alert.messageText = "ShareExtensionDisabledTitle".localized()
|
||||||
alert.informativeText = "ShareExtensionDisabledDescription".localized()
|
if #available(macOS 13.0, *) {
|
||||||
|
alert.informativeText = "ShareExtensionDisabledDescription".localized()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alert.informativeText = "ShareExtensionDisabledDescriptionLegacy".localized()
|
||||||
|
}
|
||||||
alert.addButton(withTitle: "CloseAlert".localized())
|
alert.addButton(withTitle: "CloseAlert".localized())
|
||||||
alert.runModal()
|
alert.runModal()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
//
|
||||||
|
// AlertManager.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 01.02.26.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import LUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public final class IntroductionSheetManager: ObservableObject {
|
||||||
|
|
||||||
|
public static let sharedInstance = IntroductionSheetManager()
|
||||||
|
|
||||||
|
private var sheetView: NSPanel? = nil
|
||||||
|
private var sheetAttachedWindow: NSWindow? = nil
|
||||||
|
|
||||||
|
|
||||||
|
func openIntroductionWindow(on window: NSWindow, startReceiving: @escaping () -> Void, onFinish: @escaping () -> Void) {
|
||||||
|
openSheetWindow(on: window, contentView: IntroductionView(startReceiving: startReceiving, onFinish: {
|
||||||
|
self.closeSheetWindow()
|
||||||
|
onFinish()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func openSheetWindow<Content: View>(on window: NSWindow, contentView: Content) {
|
||||||
|
if sheetView == nil {
|
||||||
|
// Create the panel
|
||||||
|
let panel = NSPanel(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: IntroductionView.width, height: IntroductionView.height),
|
||||||
|
styleMask: [.titled, .closable, .utilityWindow],
|
||||||
|
backing: .buffered,
|
||||||
|
defer: false
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set content (can be a NSHostingView for SwiftUI views)
|
||||||
|
let hostingView = NSHostingView(rootView: contentView)
|
||||||
|
panel.contentView = hostingView
|
||||||
|
|
||||||
|
// Present as sheet
|
||||||
|
window.beginSheet(panel, completionHandler: { response in
|
||||||
|
log("[IntroductionSheetManager] Sheet dismissed with response: \(response)")
|
||||||
|
})
|
||||||
|
|
||||||
|
self.sheetAttachedWindow = window
|
||||||
|
self.sheetView = panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func closeSheetWindow() {
|
||||||
|
|
||||||
|
if let mainWindow = sheetAttachedWindow, let sheetView = sheetView {
|
||||||
|
|
||||||
|
log("[IntroductionSheetManager] Closing sheetView window")
|
||||||
|
|
||||||
|
mainWindow.endSheet(sheetView)
|
||||||
|
|
||||||
|
self.sheetView = nil
|
||||||
|
self.sheetAttachedWindow = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,10 @@ struct IntroductionView: View {
|
|||||||
.padding(.top)
|
.padding(.top)
|
||||||
.minimumScaleFactor(0.5)
|
.minimumScaleFactor(0.5)
|
||||||
.opacity(0.8)
|
.opacity(0.8)
|
||||||
|
|
||||||
|
if currentPage == .enableShareExtension {
|
||||||
|
EnableExtensionView(canShowIntialCheckmark: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, 40)
|
||||||
|
|
||||||
@@ -78,8 +82,7 @@ struct IntroductionView: View {
|
|||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
if let skipAction = currentPage.skipAction,
|
if let skipAction = currentPage.skipAction, pollingPage != nil {
|
||||||
(currentPage.needRequestBeforeSkipAvailable == false || pollingPage != nil) {
|
|
||||||
|
|
||||||
Button("introduction_skip") {
|
Button("introduction_skip") {
|
||||||
self.skipAction = skipAction
|
self.skipAction = skipAction
|
||||||
@@ -105,22 +108,6 @@ struct IntroductionView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if let helpAction = currentPage.helpAction, pollingPage != nil {
|
|
||||||
HStack(spacing: 3) {
|
|
||||||
Image(systemName: "questionmark.circle")
|
|
||||||
Text(helpAction.question.localized())
|
|
||||||
}
|
|
||||||
.opacity(0.8)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
.font(.system(size: 15))
|
|
||||||
.onTapGesture {
|
|
||||||
helpActionAlert = helpAction
|
|
||||||
}
|
|
||||||
.alert(item: $helpActionAlert) { helpAction in
|
|
||||||
.init(title: Text(helpAction.question.localized()), message: Text(helpAction.answer.localized()), dismissButton: .cancel(Text("introduction_help_action_alert_close")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continueArea
|
continueArea
|
||||||
}
|
}
|
||||||
@@ -155,6 +142,7 @@ struct IntroductionView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var continueArea: some View {
|
private var continueArea: some View {
|
||||||
@@ -180,6 +168,7 @@ struct IntroductionView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func pageDidChange(to newPage: IntroductionPage) {
|
private func pageDidChange(to newPage: IntroductionPage) {
|
||||||
stopPolling()
|
stopPolling()
|
||||||
@@ -189,6 +178,7 @@ struct IntroductionView: View {
|
|||||||
pollingPage = newPage
|
pollingPage = newPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func continueTapped() {
|
private func continueTapped() {
|
||||||
// If already allowed, advance immediately
|
// If already allowed, advance immediately
|
||||||
@@ -199,41 +189,18 @@ struct IntroductionView: View {
|
|||||||
|
|
||||||
// Not allowed yet: show loading and start polling every 1s
|
// Not allowed yet: show loading and start polling every 1s
|
||||||
pollingPage = currentPage
|
pollingPage = currentPage
|
||||||
|
|
||||||
// Page-specific "kickoff" action that should happen only after user presses continue
|
|
||||||
if currentPage == .enableShareExtension {
|
|
||||||
if let url = URL(string: "x-apple.systempreferences:com.apple.LoginItems-Settings.extension") {
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentPage == .localNetworkAccess {
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
try await Task.sleep(nanoseconds: 20_000_000_000)
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
guard currentPage == .localNetworkAccess else { return }
|
|
||||||
guard skipAction == nil else { return }
|
|
||||||
|
|
||||||
if let action = currentPage.skipAction {
|
|
||||||
skipAction = action
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func triggerLocalNetworkPermission() {
|
private func triggerLocalNetworkPermission() {
|
||||||
self.startReceiving()
|
self.startReceiving()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func advance() {
|
private func advance() {
|
||||||
|
|
||||||
if currentPage == .splash {
|
if currentPage == .localNetworkAccess {
|
||||||
|
let _ = DeviceToDeviceHeuristicScanner.shared.hasLocalNetworkAccess(completion: {_ in })
|
||||||
triggerLocalNetworkPermission()
|
triggerLocalNetworkPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +211,7 @@ struct IntroductionView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func stopPolling() {
|
private func stopPolling() {
|
||||||
// Cancels the .task(id:) by changing the id and resets UI state
|
// Cancels the .task(id:) by changing the id and resets UI state
|
||||||
pollingPage = nil
|
pollingPage = nil
|
||||||
@@ -251,7 +219,6 @@ struct IntroductionView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
enum IntroductionPage: CaseIterable {
|
enum IntroductionPage: CaseIterable {
|
||||||
case splash
|
case splash
|
||||||
case noWifi
|
case noWifi
|
||||||
@@ -274,6 +241,7 @@ enum IntroductionPage: CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var subtitle: String {
|
var subtitle: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .splash:
|
case .splash:
|
||||||
@@ -281,49 +249,29 @@ enum IntroductionPage: CaseIterable {
|
|||||||
case .noWifi:
|
case .noWifi:
|
||||||
return "introduction_no_wifi_description"
|
return "introduction_no_wifi_description"
|
||||||
case .localNetworkAccess:
|
case .localNetworkAccess:
|
||||||
return "introduction_local_network_access_description"
|
return "introduction_local_network_access_description".localized(with: "introduction_continue".localized())
|
||||||
case .enableShareExtension:
|
case .enableShareExtension:
|
||||||
if #available(macOS 13.0, *) {
|
if #available(macOS 13.0, *) {
|
||||||
return "introduction_enable_share_extension_description"
|
return "introduction_enable_share_extension_description".localized(with: "EnableQuickDropExtension".localized())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "introduction_enable_share_extension_description_legacy".localized(with: "EnableQuickDropExtension".localized())
|
||||||
}
|
}
|
||||||
return "introduction_enable_share_extension_description_macos12"
|
|
||||||
case .finished:
|
case .finished:
|
||||||
return "introduction_finished_description"
|
return "introduction_finished_description"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var skipAction: SkipAction? {
|
var skipAction: SkipAction? {
|
||||||
switch self {
|
switch self {
|
||||||
case .splash:
|
|
||||||
return nil
|
|
||||||
case .noWifi:
|
case .noWifi:
|
||||||
return .init(action: presentNextPage, warningTitle: "introduction_no_wifi_skip_title", warningMessage: "introduction_no_wifi_skip_message")
|
return .init(action: presentNextPage, warningTitle: "introduction_no_wifi_skip_title", warningMessage: "introduction_no_wifi_skip_message")
|
||||||
case .localNetworkAccess:
|
|
||||||
return .init(action: presentNextPage, warningTitle: "introduction_local_network_access_skip_title", warningMessage: "introduction_local_network_access_skip_message")
|
|
||||||
case .enableShareExtension:
|
|
||||||
return .init(action: presentNextPage, warningTitle: "introduction_enable_share_extension_skip_title", warningMessage: "introduction_enable_share_extension_skip_message")
|
|
||||||
case .finished:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var helpAction: HelpAction? {
|
|
||||||
switch self {
|
|
||||||
case .enableShareExtension:
|
|
||||||
return .init(question: "introduction_enable_share_extension_not_visible", answer: "introduction_enable_share_extension_not_visible_answer")
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var needRequestBeforeSkipAvailable: Bool {
|
|
||||||
switch self {
|
|
||||||
case .localNetworkAccess:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var canPerformAction: Bool {
|
var canPerformAction: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -334,11 +282,11 @@ enum IntroductionPage: CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func presentNextPage() -> IntroductionPage? {
|
func presentNextPage() -> IntroductionPage? {
|
||||||
switch self {
|
switch self {
|
||||||
case .splash:
|
case .splash:
|
||||||
return nil
|
return NearbyConnectionManager.shared.isConnectedToLocalNetwork ? firstPermissionPane() : .noWifi
|
||||||
//NearbyConnectionManager.shared.isConnectedToLocalNetwork ? firstPermissionPane() : .noWifi
|
|
||||||
case .noWifi:
|
case .noWifi:
|
||||||
return firstPermissionPane()
|
return firstPermissionPane()
|
||||||
case .localNetworkAccess:
|
case .localNetworkAccess:
|
||||||
@@ -350,6 +298,7 @@ enum IntroductionPage: CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func firstPermissionPane() -> IntroductionPage {
|
private func firstPermissionPane() -> IntroductionPage {
|
||||||
if #available(macOS 15.0, *) {
|
if #available(macOS 15.0, *) {
|
||||||
// Local network access is requested first on macOS 15+
|
// Local network access is requested first on macOS 15+
|
||||||
@@ -358,19 +307,17 @@ enum IntroductionPage: CaseIterable {
|
|||||||
return .enableShareExtension
|
return .enableShareExtension
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func canContinue() -> Bool {
|
func canContinue() -> Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .noWifi:
|
case .noWifi:
|
||||||
return NearbyConnectionManager.shared.isConnectedToLocalNetwork
|
return NearbyConnectionManager.shared.isConnectedToLocalNetwork
|
||||||
case .enableShareExtension:
|
|
||||||
return NSSharingService(named: NSSharingService.Name("com.leonboettger.neardrop.ShareExtension")) != nil
|
|
||||||
case .localNetworkAccess:
|
|
||||||
return DeviceToDeviceHeuristicScanner.shared.hasLocalNetworkAccess(completion: {_ in })
|
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func topHeader(with height: CGFloat) -> some View {
|
func topHeader(with height: CGFloat) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
||||||
@@ -395,6 +342,7 @@ enum IntroductionPage: CaseIterable {
|
|||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func imageView(for image: String, with height: CGFloat) -> some View {
|
private func imageView(for image: String, with height: CGFloat) -> some View {
|
||||||
Image(systemName: image)
|
Image(systemName: image)
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -403,6 +351,7 @@ enum IntroductionPage: CaseIterable {
|
|||||||
.padding(.top, height * 0.13)
|
.padding(.top, height * 0.13)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct SkipAction: Identifiable {
|
struct SkipAction: Identifiable {
|
||||||
let action: () -> IntroductionPage?
|
let action: () -> IntroductionPage?
|
||||||
let warningTitle: String
|
let warningTitle: String
|
||||||
@@ -411,6 +360,7 @@ enum IntroductionPage: CaseIterable {
|
|||||||
var id: String { warningTitle }
|
var id: String { warningTitle }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct HelpAction: Identifiable {
|
struct HelpAction: Identifiable {
|
||||||
let question: String
|
let question: String
|
||||||
let answer: String
|
let answer: String
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
//
|
||||||
|
// PairingQrSheetManager.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 2026-03-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class PairingQrSheetManager: NSObject, NSWindowDelegate {
|
||||||
|
static let shared = PairingQrSheetManager()
|
||||||
|
|
||||||
|
private var alert: NSAlert?
|
||||||
|
private weak var alertWindow: NSWindow?
|
||||||
|
|
||||||
|
func open(
|
||||||
|
token: String,
|
||||||
|
receiverFingerprint: String,
|
||||||
|
deviceName: String,
|
||||||
|
useCase: PairingUseCase
|
||||||
|
) {
|
||||||
|
guard alert == nil else { return }
|
||||||
|
guard let qrImage = PairingToken.makeQrImage(
|
||||||
|
token: token,
|
||||||
|
receiverFingerprint: receiverFingerprint,
|
||||||
|
useCase: useCase
|
||||||
|
) else { return }
|
||||||
|
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "PairingQrSheetTitle".localized()
|
||||||
|
alert.informativeText = "PairingQrSheetDescription".localized(with: deviceName)
|
||||||
|
alert.addButton(withTitle: "Cancel".localized())
|
||||||
|
alert.alertStyle = .informational
|
||||||
|
|
||||||
|
let accessoryView = NSHostingView(rootView: PairingQrAccessoryView(qrImage: qrImage))
|
||||||
|
accessoryView.frame = NSRect(x: 0, y: 0, width: 180, height: 180)
|
||||||
|
alert.accessoryView = accessoryView
|
||||||
|
alert.layout()
|
||||||
|
alert.buttons.forEach { button in
|
||||||
|
button.target = self
|
||||||
|
button.action = #selector(handleAlertButton(_:))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.alert = alert
|
||||||
|
let alertWindow = alert.window
|
||||||
|
alertWindow.delegate = self
|
||||||
|
self.alertWindow = alertWindow
|
||||||
|
if let screen = NSApp.mainWindow?.screen ?? NSScreen.main {
|
||||||
|
let frame = alertWindow.frame
|
||||||
|
let screenFrame = screen.visibleFrame
|
||||||
|
let origin = NSPoint(
|
||||||
|
x: screenFrame.midX - (frame.width / 2),
|
||||||
|
y: screenFrame.midY - (frame.height / 2)
|
||||||
|
)
|
||||||
|
alertWindow.setFrameOrigin(origin)
|
||||||
|
} else {
|
||||||
|
alertWindow.center()
|
||||||
|
}
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
alertWindow.makeKeyAndOrderFront(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func close() {
|
||||||
|
alertWindow?.close()
|
||||||
|
alert = nil
|
||||||
|
alertWindow = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowWillClose(_ notification: Notification) {
|
||||||
|
alert = nil
|
||||||
|
alertWindow = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleAlertButton(_ sender: NSButton) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PairingQrAccessoryView: View {
|
||||||
|
let qrImage: Image
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.fill(Color.white)
|
||||||
|
qrImage
|
||||||
|
.resizable()
|
||||||
|
.interpolation(.none)
|
||||||
|
.aspectRatio(1, contentMode: .fit)
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
.frame(width: 160, height: 160)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
//
|
|
||||||
// IncomingFileTransmissionView.swift
|
|
||||||
// QuickDrop
|
|
||||||
//
|
|
||||||
// Created by Leon Böttger on 05.10.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import AppKit
|
|
||||||
|
|
||||||
let toastViewSize = CGSize(width: 400, height: 100)
|
|
||||||
|
|
||||||
struct QuickDropToastView: View {
|
|
||||||
@ObservedObject var settings = Settings.shared
|
|
||||||
@ObservedObject var receiveModel: ReceiveModel
|
|
||||||
|
|
||||||
@State var autoHider = DispatchWorkItem(block: {})
|
|
||||||
|
|
||||||
/// Cancel while receiving.
|
|
||||||
public var onCancel: () -> Void
|
|
||||||
|
|
||||||
init(
|
|
||||||
receiveModel: ReceiveModel,
|
|
||||||
onCancel: @escaping () -> Void = {}
|
|
||||||
) {
|
|
||||||
self.receiveModel = receiveModel
|
|
||||||
self.onCancel = onCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
|
|
||||||
let isDone = receiveModel.toastActions != nil
|
|
||||||
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
ZStack(alignment: .bottomLeading) {
|
|
||||||
Image(.quickDropIcon)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFill()
|
|
||||||
.frame(width: 44, height: 44)
|
|
||||||
.clipShape(Circle())
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(String("QuickDrop"))
|
|
||||||
.font(.system(size: 16, weight: .semibold))
|
|
||||||
|
|
||||||
Text(isDone ? "FileTransferCompleted" : "Receiving")
|
|
||||||
.font(.system(size: 13))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(minLength: 12)
|
|
||||||
}
|
|
||||||
|
|
||||||
ZStack {
|
|
||||||
if let actions = receiveModel.toastActions {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
|
|
||||||
QuickDropToastViewButton(title: "OpenInFinder") {
|
|
||||||
actions.openFilesAction()
|
|
||||||
actions.closeToastAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
if let onImportToPhotos = actions.importPhotosAction {
|
|
||||||
QuickDropToastViewButton(title: "ImportToPhotos") {
|
|
||||||
onImportToPhotos()
|
|
||||||
actions.closeToastAction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
QuickDropToastViewButton(title: "Done", action: actions.closeToastAction)
|
|
||||||
.onAppear {
|
|
||||||
self.autoHider = DispatchWorkItem(block: {
|
|
||||||
actions.closeToastAction()
|
|
||||||
})
|
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: self.autoHider)
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
self.autoHider.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
ZStack {
|
|
||||||
CapsuleProgress(value: 0.5)
|
|
||||||
.opacity(0)
|
|
||||||
|
|
||||||
if let progress = receiveModel.progress {
|
|
||||||
CapsuleProgress(value: progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.animation(.easeInOut, value: receiveModel.progress == nil)
|
|
||||||
|
|
||||||
Button(action: onCancel) {
|
|
||||||
Image(systemName: "xmark")
|
|
||||||
.font(.system(size: 10, weight: .semibold))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.frame(width: 18, height: 18)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
.opacity(isDone ? 0 : 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(16)
|
|
||||||
.background(
|
|
||||||
VisualEffectView(material: .hudWindow)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
||||||
)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
||||||
.stroke(Color.black.opacity(0.06), lineWidth: 1)
|
|
||||||
)
|
|
||||||
.shadow(color: Color.black.opacity(0.1), radius: 20)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fileprivate struct QuickDropToastViewButton: View {
|
|
||||||
let title: String
|
|
||||||
let action: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: action) {
|
|
||||||
Text(title.localized())
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
.background(
|
|
||||||
Color.gray.opacity(0.15)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain) // prevents blue macOS button look
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// NSVisualEffectView wrapper for macOS 11 compatible blur/vibrancy
|
|
||||||
struct VisualEffectView: NSViewRepresentable {
|
|
||||||
var material: NSVisualEffectView.Material = .hudWindow
|
|
||||||
var blendingMode: NSVisualEffectView.BlendingMode = .withinWindow
|
|
||||||
var state: NSVisualEffectView.State = .active
|
|
||||||
|
|
||||||
func makeNSView(context: Context) -> NSVisualEffectView {
|
|
||||||
let v = NSVisualEffectView()
|
|
||||||
v.material = material
|
|
||||||
v.blendingMode = blendingMode
|
|
||||||
v.state = state
|
|
||||||
v.isEmphasized = true
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateNSView(_ v: NSVisualEffectView, context: Context) {
|
|
||||||
v.material = material
|
|
||||||
v.blendingMode = blendingMode
|
|
||||||
v.state = state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Capsule-style linear progress (0...1)
|
|
||||||
struct CapsuleProgress: View {
|
|
||||||
var value: Double
|
|
||||||
var track = Color(NSColor.separatorColor).opacity(0.28)
|
|
||||||
var fill = Color(NSColor.systemBlue)
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
GeometryReader { geo in
|
|
||||||
let progress = max(0, min(1, value))
|
|
||||||
ZStack(alignment: .leading) {
|
|
||||||
Capsule()
|
|
||||||
.fill(track)
|
|
||||||
Capsule()
|
|
||||||
.fill(fill)
|
|
||||||
.mask(
|
|
||||||
Capsule()
|
|
||||||
.frame(width: progress * geo.size.width)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: 8)
|
|
||||||
.animation(.easeInOut(duration: 0.25), value: value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Preview
|
|
||||||
struct QuickDropToastView_Previews: PreviewProvider {
|
|
||||||
struct Demo: View {
|
|
||||||
@State var model = ReceiveModel(controlPlusScreen: { _ in })
|
|
||||||
@State var done = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
QuickDropToastView(
|
|
||||||
receiveModel: model,
|
|
||||||
onCancel: { }
|
|
||||||
)
|
|
||||||
.frame(width: toastViewSize.width, height: toastViewSize.height)
|
|
||||||
.padding(100)
|
|
||||||
.background(Color.white.opacity(0.2))
|
|
||||||
.onAppear {
|
|
||||||
model.progress = 0.01
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static var previews: some View { Demo() }
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ import LUI
|
|||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
|
|
||||||
@ObservedObject var settings = Settings.shared
|
@ObservedObject var settings = Settings.sharedInstance
|
||||||
@ObservedObject var connectionManager = NearbyConnectionManager.shared
|
@ObservedObject var connectionManager = NearbyConnectionManager.shared
|
||||||
|
|
||||||
@State private var isChangeDeviceNameAlertPresented = false
|
@State private var isChangeDeviceNameAlertPresented = false
|
||||||
@@ -48,9 +48,8 @@ struct SettingsView: View {
|
|||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
|
|
||||||
Button("Edit") {
|
Button("Edit") {
|
||||||
isChangeDeviceNameAlertPresented.toggle()
|
presentRenameAlert()
|
||||||
}
|
}
|
||||||
.changeDeviceNameAlert(isPresented: $isChangeDeviceNameAlertPresented)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
@@ -116,8 +115,8 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
let bookmarkData = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
|
let bookmarkData = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
|
||||||
|
|
||||||
Settings.shared.saveFolderBookmark = bookmarkData
|
Settings.sharedInstance.saveFolderBookmark = bookmarkData
|
||||||
Settings.shared.openFinderAfterReceiving = true
|
Settings.sharedInstance.openFinderAfterReceiving = true
|
||||||
} catch {
|
} catch {
|
||||||
log("Failed to save security-scoped bookmark: \(error)")
|
log("Failed to save security-scoped bookmark: \(error)")
|
||||||
|
|
||||||
@@ -149,12 +148,47 @@ struct SettingsView: View {
|
|||||||
window.center()
|
window.center()
|
||||||
window.isReleasedWhenClosed = false
|
window.isReleasedWhenClosed = false
|
||||||
window.title = "TrustedDevices".localized()
|
window.title = "TrustedDevices".localized()
|
||||||
|
window.backgroundColor = NSColor(Color.defaultMacBackgroundColor)
|
||||||
window.contentView = NSHostingView(rootView: TrustedDevicesView())
|
window.contentView = NSHostingView(rootView: TrustedDevicesView())
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
window.level = .normal
|
window.level = .normal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func presentRenameAlert() {
|
||||||
|
guard let window = NSApplication.shared.keyWindow else { return }
|
||||||
|
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "ChangeDeviceName".localized()
|
||||||
|
alert.informativeText = "ChangeDeviceNameDescription".localized()
|
||||||
|
alert.alertStyle = .informational
|
||||||
|
|
||||||
|
let textField = NSTextField(string: NearbyConnectionManager.shared.deviceInfo.name ?? "")
|
||||||
|
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
textField.frame = NSRect(x: 0, y: 0, width: 260, height: 24)
|
||||||
|
alert.accessoryView = textField
|
||||||
|
|
||||||
|
alert.addButton(withTitle: "Save".localized())
|
||||||
|
alert.addButton(withTitle: "Cancel".localized())
|
||||||
|
|
||||||
|
alert.beginSheetModal(for: window) { response in
|
||||||
|
|
||||||
|
if response == .alertFirstButtonReturn {
|
||||||
|
let trimmed = textField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
NearbyConnectionManager.shared.setCustomDeviceName(to: trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let alertWindow = alert.window
|
||||||
|
alertWindow.makeFirstResponder(textField)
|
||||||
|
if let editor = alertWindow.fieldEditor(true, for: textField) as? NSTextView {
|
||||||
|
editor.selectedRange = NSRange(location: 0, length: textField.stringValue.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
//
|
||||||
|
// TrustedDevicesView.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 23.08.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import LUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TrustedDevicesView: View {
|
||||||
|
@StateObject private var store = TrustStore.shared
|
||||||
|
|
||||||
|
@State private var deviceToRemove: String? = nil
|
||||||
|
@State private var showRemoveAlert = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationSubView(header: isMac() ? "" : "TrustedDevices") {
|
||||||
|
VStack {
|
||||||
|
if store.trustedCertificates.isEmpty {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Text("NoTrustedDevices")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("TrustedDevicesDescription")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding()
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
} else {
|
||||||
|
CustomSection(footer: "TrustedDevicesDescription") {
|
||||||
|
devicesView
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, isMac() ? 18 : 0)
|
||||||
|
}
|
||||||
|
.animation(.default, value: store.trustedCertificates)
|
||||||
|
.alert(isPresented: $showRemoveAlert) {
|
||||||
|
Alert(
|
||||||
|
title: Text("RemoveTrustedDeviceAlertTitle"),
|
||||||
|
message: Text("RemoveTrustedDeviceAlertDescription"),
|
||||||
|
primaryButton: .destructive(Text("Remove")) {
|
||||||
|
if let key = deviceToRemove {
|
||||||
|
store.removeTrusted(secretIdHex: key)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var devicesView: some View {
|
||||||
|
let entries = store.trustedCertificates.sorted { $0.value.creationDate < $1.value.creationDate }
|
||||||
|
return ForEach(entries, id: \.key) { key, cert in
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
LUIText(cert.device.name ?? "Unknown".localized(), isBold: true)
|
||||||
|
LUIText(Self.dateFormatter.string(from: cert.creationDate), color: .mainColor.opacity(0.7))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
LUIButton {
|
||||||
|
deviceToRemove = key
|
||||||
|
showRemoveAlert = true
|
||||||
|
} label: {
|
||||||
|
ReorderListIcon(imageName: "minus.circle.fill", color: .red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let dateFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
formatter.timeStyle = .short
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
TrustedDevicesView()
|
||||||
|
.onAppear {
|
||||||
|
|
||||||
|
var cert1 = Sharing_Nearby_PublicCertificate()
|
||||||
|
cert1.secretID = Data([0x01, 0x02, 0x03, 0x04])
|
||||||
|
|
||||||
|
TrustStore.shared.addTrusted(certificate: cert1, device: RemoteDeviceInfo(name: "MacBook Pro", type: .computer))
|
||||||
|
|
||||||
|
|
||||||
|
var cert2 = Sharing_Nearby_PublicCertificate()
|
||||||
|
cert2.secretID = Data([0x0A, 0x0B, 0x0C, 0x0D])
|
||||||
|
|
||||||
|
TrustStore.shared.addTrusted(certificate: cert2, device: RemoteDeviceInfo(name: "iPhone", type: .phone))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,646 @@
|
|||||||
|
//
|
||||||
|
// IncomingFileTransmissionView.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 05.10.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
import LUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
let toastViewSize = CGSize(width: 340, height: 65)
|
||||||
|
let toastCornerRadius: CGFloat = 22
|
||||||
|
let toastTrailingPadding: CGFloat = 20
|
||||||
|
let toastSlideDistance: CGFloat = 420
|
||||||
|
let toastSlideAnimationDuration: TimeInterval = 0.5
|
||||||
|
|
||||||
|
struct QuickDropToastView: View {
|
||||||
|
@ObservedObject var settings = Settings.sharedInstance
|
||||||
|
@ObservedObject var receiveModel: ReceiveModel
|
||||||
|
let animateProgressVisibility: Bool
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
@State var autoHider = DispatchWorkItem(block: {})
|
||||||
|
@State private var isHovering = false
|
||||||
|
|
||||||
|
/// Cancel while receiving.
|
||||||
|
public var onCancel: () -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
receiveModel: ReceiveModel,
|
||||||
|
animateProgressVisibility: Bool = true,
|
||||||
|
onCancel: @escaping () -> Void = {}
|
||||||
|
) {
|
||||||
|
self.receiveModel = receiveModel
|
||||||
|
self.animateProgressVisibility = animateProgressVisibility
|
||||||
|
self.onCancel = onCancel
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startAutoHide(_ actions: ReceiveModel.ToastViewAction) {
|
||||||
|
autoHider.cancel()
|
||||||
|
autoHider = DispatchWorkItem(block: {
|
||||||
|
actions.autoHideAction()
|
||||||
|
})
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + actions.autoHideDelay, execute: autoHider)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startAutoHideIfPossible() {
|
||||||
|
guard !isHovering, let actions = receiveModel.toastActions else { return }
|
||||||
|
startAutoHide(actions)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
|
||||||
|
let consent = receiveModel.consentState
|
||||||
|
let actions = receiveModel.toastActions
|
||||||
|
let hasActionButtons = actions?.openFilesAction != nil || actions?.importPhotosAction != nil
|
||||||
|
let deviceName = receiveModel.activeDeviceName ?? "AndroidDevice".localized()
|
||||||
|
let isNotificationSyncToast = consent?.notificationSyncStage != nil
|
||||||
|
let isNotificationSyncConsent = consent?.notificationSyncStage == .consent
|
||||||
|
let isNotificationSyncPinStage = consent?.notificationSyncStage == .pin
|
||||||
|
let showConsentActions = consent != nil && (!isNotificationSyncToast || isNotificationSyncConsent)
|
||||||
|
let showsRightColumn = showConsentActions || hasActionButtons
|
||||||
|
|
||||||
|
let subHeaderSize = 21.0
|
||||||
|
|
||||||
|
let closeButtonVisible = isHovering && (actions != nil || isNotificationSyncPinStage)
|
||||||
|
|
||||||
|
ZStack(alignment: .topTrailing) {
|
||||||
|
HStack(alignment: .center, spacing: 12) {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
|
||||||
|
let iconSize = 32.0
|
||||||
|
|
||||||
|
let headerFont = Font.system(.body).bold()
|
||||||
|
let subheaderFont = Font.system(.body)
|
||||||
|
let subheaderColor = Color.secondary
|
||||||
|
|
||||||
|
AppIconView(hasPlusIcon: false, size: iconSize, supportsDarkModeShadow: false)
|
||||||
|
.frame(width: iconSize, height: iconSize)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
if let consent = consent {
|
||||||
|
if isNotificationSyncToast {
|
||||||
|
HStack {
|
||||||
|
Text(String("QuickDrop"))
|
||||||
|
.font(headerFont)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Text("FromDevice".localized(with: deviceName))
|
||||||
|
.font(subheaderFont)
|
||||||
|
.foregroundColor(subheaderColor)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(consent.message)
|
||||||
|
.font(subheaderFont)
|
||||||
|
.foregroundColor(subheaderColor)
|
||||||
|
.lineLimit(2)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
} else {
|
||||||
|
Text(verbatim: "QuickDrop | \(consent.pinCodeMessage)")
|
||||||
|
.font(headerFont)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Text(consent.message)
|
||||||
|
.font(subheaderFont)
|
||||||
|
.foregroundColor(subheaderColor)
|
||||||
|
.lineLimit(2)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HStack {
|
||||||
|
Text(String("QuickDrop"))
|
||||||
|
.font(headerFont)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
if consent == nil, actions == nil {
|
||||||
|
Text("FromDevice".localized(with: deviceName))
|
||||||
|
.font(subheaderFont)
|
||||||
|
.foregroundColor(subheaderColor)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if consent == nil, actions == nil {
|
||||||
|
HStack {
|
||||||
|
ZStack {
|
||||||
|
CapsuleProgress(value: 0.5)
|
||||||
|
.opacity(0)
|
||||||
|
|
||||||
|
if let progress = receiveModel.progress {
|
||||||
|
CapsuleProgress(value: progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(
|
||||||
|
animateProgressVisibility
|
||||||
|
? .easeInOut
|
||||||
|
: .easeInOut.delay(toastSlideAnimationDuration),
|
||||||
|
value: receiveModel.progress == nil
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(action: onCancel) {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
.font(.system(size: 10, weight: .semibold))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(width: 18, height: 18)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
.padding(.trailing, 16)
|
||||||
|
.frame(height: subHeaderSize)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
Text(LocalizedStringKey(actions?.completionMessageKey ?? "FileTransferCompleted"))
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.frame(height: subHeaderSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
if showsRightColumn {
|
||||||
|
ActionColumn() {
|
||||||
|
if showConsentActions, let consent = consent {
|
||||||
|
ActionButtonRow {
|
||||||
|
QuickDropToastViewButton(title: "Decline") {
|
||||||
|
consent.declineAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionButtonRow {
|
||||||
|
if consent.allowsTrust {
|
||||||
|
QuickDropToastViewMenuButton(title: "Accept") {
|
||||||
|
Button("AcceptOnce".localized()) {
|
||||||
|
consent.acceptAction(false)
|
||||||
|
}
|
||||||
|
Button("AutoAcceptFromThisDevice".localized()) {
|
||||||
|
consent.acceptAction(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QuickDropToastViewButton(title: "Accept") {
|
||||||
|
consent.acceptAction(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let actions = actions {
|
||||||
|
if let openFilesAction = actions.openFilesAction {
|
||||||
|
ActionButtonRow {
|
||||||
|
QuickDropToastViewButton(title: "OpenInFinder") {
|
||||||
|
openFilesAction()
|
||||||
|
actions.closeToastAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let onImportToPhotos = actions.importPhotosAction {
|
||||||
|
|
||||||
|
ActionButtonRow {
|
||||||
|
QuickDropToastViewButton(title: "ImportToPhotos") {
|
||||||
|
onImportToPhotos()
|
||||||
|
actions.closeToastAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.leading, 16)
|
||||||
|
.frame(width: toastViewSize.width, height: toastViewSize.height)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onHover { hovering in
|
||||||
|
withAnimation(.easeInOut(duration: 0.15)) {
|
||||||
|
isHovering = hovering
|
||||||
|
}
|
||||||
|
|
||||||
|
if hovering {
|
||||||
|
autoHider.cancel()
|
||||||
|
} else {
|
||||||
|
startAutoHideIfPossible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: receiveModel.toastActions != nil) { hasActions in
|
||||||
|
if hasActions {
|
||||||
|
startAutoHideIfPossible()
|
||||||
|
} else {
|
||||||
|
autoHider.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
startAutoHideIfPossible()
|
||||||
|
}
|
||||||
|
.background(
|
||||||
|
AdaptiveToastBackgroundLayer(cornerRadius: toastCornerRadius)
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: toastCornerRadius, style: .continuous)
|
||||||
|
.stroke((colorScheme.isLight ? Color.white.opacity(0.5) : Color.gray.opacity(0.33)), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.overlay(alignment: .topLeading) {
|
||||||
|
if closeButtonVisible {
|
||||||
|
Button(action: {
|
||||||
|
if let actions = actions {
|
||||||
|
actions.closeToastAction()
|
||||||
|
} else if isNotificationSyncPinStage {
|
||||||
|
receiveModel.hideQuickDropToast(style: .fade)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
.font(.system(size: 9, weight: .semibold))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(width: 18, height: 18)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(Color(NSColor.windowBackgroundColor).opacity(0.9))
|
||||||
|
.shadow(color: Color.black.opacity(0.12), radius: 2, x: 0, y: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
.padding(.trailing, 3)
|
||||||
|
.onHover { hovering in
|
||||||
|
if hovering {
|
||||||
|
withAnimation(.easeInOut(duration: 0.15)) {
|
||||||
|
isHovering = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transition(.opacity.combined(with: .scale(scale: 0.95)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.shadow(color: Color.black.opacity(colorScheme.isLight ? 0.16 : 0.30), radius: 10, x: 0, y: 5)
|
||||||
|
.shadow(color: Color.black.opacity(colorScheme.isLight ? 0.06 : 0.14), radius: 2, x: 0, y: 1)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct QuickDropToastHostView: View {
|
||||||
|
@ObservedObject var receiveModel: ReceiveModel
|
||||||
|
let onCancel: () -> Void
|
||||||
|
|
||||||
|
@State private var didRenderOnce = false
|
||||||
|
@State private var hasFinishedSlideIn = false
|
||||||
|
|
||||||
|
private let slideAnimation = Animation.smooth
|
||||||
|
private let fadeAnimation = Animation.easeInOut
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let isVisible = didRenderOnce && receiveModel.toastIsVisible
|
||||||
|
let isSlide = receiveModel.toastDismissStyle == .slide
|
||||||
|
let hiddenOffset: CGFloat = isSlide ? toastSlideDistance : 0
|
||||||
|
let hiddenOpacity: Double = isSlide ? 1 : 0
|
||||||
|
let hiddenBlur: CGFloat = isSlide ? 0 : 8
|
||||||
|
let animation = isSlide ? slideAnimation : fadeAnimation
|
||||||
|
|
||||||
|
QuickDropToastView(
|
||||||
|
receiveModel: receiveModel,
|
||||||
|
animateProgressVisibility: hasFinishedSlideIn,
|
||||||
|
onCancel: onCancel
|
||||||
|
)
|
||||||
|
.frame(width: toastViewSize.width, height: toastViewSize.height)
|
||||||
|
.offset(x: isVisible ? 0 : hiddenOffset)
|
||||||
|
.opacity(isVisible ? 1 : hiddenOpacity)
|
||||||
|
.blur(radius: isVisible ? 0 : hiddenBlur)
|
||||||
|
.animation(animation, value: isVisible)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
|
||||||
|
.padding(.trailing, toastTrailingPadding)
|
||||||
|
.onAppear {
|
||||||
|
// Defer the visibility gate one run-loop tick so initial state is
|
||||||
|
// always rendered hidden before we animate to visible.
|
||||||
|
guard !didRenderOnce else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
didRenderOnce = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task(id: isVisible) {
|
||||||
|
hasFinishedSlideIn = !isVisible || !isSlide
|
||||||
|
|
||||||
|
guard isVisible, isSlide else { return }
|
||||||
|
|
||||||
|
try? await Task.sleep(nanoseconds: UInt64(toastSlideAnimationDuration * 1_000_000_000))
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
|
hasFinishedSlideIn = true
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
hasFinishedSlideIn = false
|
||||||
|
didRenderOnce = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fileprivate struct QuickDropToastViewButton: View {
|
||||||
|
let title: String
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
ToastButtonLabel(title: title, fillsWidth: false, showsBackground: true)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain) // prevents blue macOS button look
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fileprivate struct QuickDropToastViewMenuButton<Content: View>: View {
|
||||||
|
let title: String
|
||||||
|
@ViewBuilder let content: () -> Content
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
ToastButtonLabel(title: title, fillsWidth: false, showsBackground: true)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
content()
|
||||||
|
} label: {
|
||||||
|
ToastButtonLabel(title: title, fillsWidth: false, showsBackground: false)
|
||||||
|
}
|
||||||
|
.menuStyle(BorderlessButtonMenuStyle(showsMenuIndicator: false))
|
||||||
|
}
|
||||||
|
.fixedSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fileprivate struct ToastButtonLabel: View {
|
||||||
|
let title: String
|
||||||
|
let fillsWidth: Bool
|
||||||
|
let showsBackground: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(title.localized())
|
||||||
|
.font(.system(size: 11.5, weight: .medium))
|
||||||
|
.foregroundColor(.mainColor.opacity(showsBackground ? 0.75 : 0))
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 7)
|
||||||
|
.padding(.vertical, 3.5)
|
||||||
|
.background(Color.gray.opacity(showsBackground ? 0.1 : 0).cornerRadius(10))
|
||||||
|
.padding(.horizontal, 7)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fileprivate struct ActionColumn<Content: View>: View {
|
||||||
|
@ViewBuilder let content: () -> Content
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .trailing, spacing: 0) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.padding(.trailing, 4)
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fileprivate struct ActionButtonRow<Content: View>: View {
|
||||||
|
@ViewBuilder let content: () -> Content
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fileprivate struct ActionDividerLine: View {
|
||||||
|
var body: some View {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(.mainColor.opacity(0.05))
|
||||||
|
.frame(height: 2)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct AdaptiveToastBackgroundLayer: View {
|
||||||
|
let cornerRadius: CGFloat
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
GlassEffectBackgroundView(cornerRadius: cornerRadius)
|
||||||
|
} else {
|
||||||
|
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow, cornerRadius: cornerRadius)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@available(macOS 26.0, *)
|
||||||
|
struct GlassEffectBackgroundView: NSViewRepresentable {
|
||||||
|
var cornerRadius: CGFloat = 0
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> NSGlassEffectView {
|
||||||
|
let v = NSGlassEffectView()
|
||||||
|
v.cornerRadius = cornerRadius
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ v: NSGlassEffectView, context: Context) {
|
||||||
|
v.cornerRadius = cornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// NSVisualEffectView wrapper for macOS 11 compatible blur/vibrancy
|
||||||
|
struct VisualEffectView: NSViewRepresentable {
|
||||||
|
var material: NSVisualEffectView.Material = .hudWindow
|
||||||
|
var blendingMode: NSVisualEffectView.BlendingMode = .withinWindow
|
||||||
|
var state: NSVisualEffectView.State = .active
|
||||||
|
var cornerRadius: CGFloat = 0
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> NSVisualEffectView {
|
||||||
|
let v = NSVisualEffectView()
|
||||||
|
v.material = material
|
||||||
|
v.blendingMode = blendingMode
|
||||||
|
v.state = state
|
||||||
|
v.isEmphasized = true
|
||||||
|
if cornerRadius > 0 {
|
||||||
|
v.wantsLayer = true
|
||||||
|
v.layer?.cornerRadius = cornerRadius
|
||||||
|
v.layer?.masksToBounds = true
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ v: NSVisualEffectView, context: Context) {
|
||||||
|
v.material = material
|
||||||
|
v.blendingMode = blendingMode
|
||||||
|
v.state = state
|
||||||
|
if cornerRadius > 0 {
|
||||||
|
v.wantsLayer = true
|
||||||
|
v.layer?.cornerRadius = cornerRadius
|
||||||
|
v.layer?.masksToBounds = true
|
||||||
|
} else {
|
||||||
|
v.layer?.cornerRadius = 0
|
||||||
|
v.layer?.masksToBounds = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Capsule-style linear progress (0...1)
|
||||||
|
struct CapsuleProgress: View {
|
||||||
|
var value: Double
|
||||||
|
var track = Color(NSColor.systemBlue).opacity(0.28)
|
||||||
|
var fill = Color(NSColor.systemBlue)
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geo in
|
||||||
|
let progress = max(0, min(1, value))
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
Capsule()
|
||||||
|
.fill(track)
|
||||||
|
Capsule()
|
||||||
|
.fill(fill)
|
||||||
|
.mask(
|
||||||
|
Capsule()
|
||||||
|
.frame(width: progress * geo.size.width)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 6)
|
||||||
|
.animation(.easeInOut(duration: 0.25), value: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
struct QuickDropToastView_Previews: PreviewProvider {
|
||||||
|
struct ConsentDemo: View {
|
||||||
|
@State var model = ReceiveModel(controlPlusScreen: { _ in })
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
QuickDropToastView(
|
||||||
|
receiveModel: model,
|
||||||
|
onCancel: { }
|
||||||
|
)
|
||||||
|
.frame(width: toastViewSize.width, height: toastViewSize.height)
|
||||||
|
.clipped()
|
||||||
|
.padding(100)
|
||||||
|
.background(QuickDropToastView_Previews.previewWallpaper)
|
||||||
|
.onAppear {
|
||||||
|
model.consentState = .init(
|
||||||
|
transferID: "preview",
|
||||||
|
pinCodeMessage: "PIN: 1233",
|
||||||
|
message: "45 images from Pixel 6 Pro von Leon",
|
||||||
|
notificationSyncStage: nil,
|
||||||
|
allowsTrust: true,
|
||||||
|
acceptAction: { _ in },
|
||||||
|
declineAction: { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct ConsentDemo2: View {
|
||||||
|
@State var model = ReceiveModel(controlPlusScreen: { _ in })
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
QuickDropToastView(
|
||||||
|
receiveModel: model,
|
||||||
|
onCancel: { }
|
||||||
|
)
|
||||||
|
.frame(width: toastViewSize.width, height: toastViewSize.height)
|
||||||
|
.clipped()
|
||||||
|
.padding(100)
|
||||||
|
.background(QuickDropToastView_Previews.previewWallpaper)
|
||||||
|
.onAppear {
|
||||||
|
model.consentState = .init(
|
||||||
|
transferID: "preview",
|
||||||
|
pinCodeMessage: "PIN: 1233",
|
||||||
|
message: "45 images from Pixel 6 Pro",
|
||||||
|
notificationSyncStage: nil,
|
||||||
|
allowsTrust: false,
|
||||||
|
acceptAction: { _ in },
|
||||||
|
declineAction: { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct ProgressDemo: View {
|
||||||
|
@State var model = ReceiveModel(controlPlusScreen: { _ in })
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
QuickDropToastView(
|
||||||
|
receiveModel: model,
|
||||||
|
onCancel: { }
|
||||||
|
)
|
||||||
|
.frame(width: toastViewSize.width, height: toastViewSize.height)
|
||||||
|
.clipped()
|
||||||
|
.padding(100)
|
||||||
|
.background(QuickDropToastView_Previews.previewWallpaper)
|
||||||
|
.onAppear {
|
||||||
|
model.progress = 0.42
|
||||||
|
model.toastActions = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct CompletedDemo: View {
|
||||||
|
@State var model = ReceiveModel(controlPlusScreen: { _ in })
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
QuickDropToastView(
|
||||||
|
receiveModel: model,
|
||||||
|
onCancel: { }
|
||||||
|
)
|
||||||
|
.frame(width: toastViewSize.width, height: toastViewSize.height)
|
||||||
|
.clipped()
|
||||||
|
.padding(100)
|
||||||
|
.background(previewWallpaper)
|
||||||
|
.onAppear {
|
||||||
|
model.progress = 1
|
||||||
|
model.toastActions = .init(completionMessageKey: "Saved", autoHideDelay: 10, openFilesAction: {}, importPhotosAction: {}, closeToastAction: {}, autoHideAction: {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static let previewWallpaper = LinearGradient(
|
||||||
|
gradient: Gradient(colors: [
|
||||||
|
Color(red: 0.92, green: 0.82, blue: 0.94),
|
||||||
|
Color(red: 0.86, green: 0.76, blue: 0.90),
|
||||||
|
Color(red: 0.78, green: 0.70, blue: 0.86)
|
||||||
|
]),
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
ConsentDemo()
|
||||||
|
ConsentDemo2()
|
||||||
|
ProgressDemo()
|
||||||
|
CompletedDemo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
//
|
|
||||||
// TrustedDevicesView.swift
|
|
||||||
// QuickDrop
|
|
||||||
//
|
|
||||||
// Created by Leon Böttger on 23.08.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
#if !os(macOS)
|
|
||||||
import LUI
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct TrustedDevicesView: View {
|
|
||||||
@StateObject private var store = TrustStore.shared
|
|
||||||
|
|
||||||
@State private var deviceToRemove: String? = nil
|
|
||||||
@State private var showRemoveAlert = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
if store.trustedCertificates.isEmpty {
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
Text("NoTrustedDevices")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text("TrustedDevicesDescription")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.padding()
|
|
||||||
.frame(maxHeight: .infinity)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
#if !os(macOS)
|
|
||||||
CustomSection(footer: "TrustedDevicesDescription") {
|
|
||||||
devicesView
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
List {
|
|
||||||
devicesView
|
|
||||||
|
|
||||||
Text("TrustedDevicesDescription")
|
|
||||||
.font(.footnote)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding(.top, 4)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.animation(.default, value: store.trustedCertificates)
|
|
||||||
.alert(isPresented: $showRemoveAlert) {
|
|
||||||
Alert(
|
|
||||||
title: Text("RemoveTrustedDeviceAlertTitle"),
|
|
||||||
message: Text("RemoveTrustedDeviceAlertDescription"),
|
|
||||||
primaryButton: .destructive(Text("Remove")) {
|
|
||||||
if let key = deviceToRemove {
|
|
||||||
store.removeTrusted(secretIdHex: key)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
secondaryButton: .cancel()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var devicesView: some View {
|
|
||||||
ForEach(Array(store.trustedCertificates.keys), id: \.self) { key in
|
|
||||||
if let cert = store.trustedCertificates[key] {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(cert.device.name ?? "Unknown".localized())
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
// macOS 11: use DateFormatter instead of .formatted
|
|
||||||
Text(Self.dateFormatter.string(from: cert.creationDate))
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Button(action: {
|
|
||||||
deviceToRemove = key
|
|
||||||
showRemoveAlert = true
|
|
||||||
}) {
|
|
||||||
Image(systemName: "minus.circle.fill")
|
|
||||||
.font(.system(size: 20))
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
.buttonStyle(BorderlessButtonStyle())
|
|
||||||
}
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - DateFormatter for macOS 11
|
|
||||||
private static let dateFormatter: DateFormatter = {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .medium
|
|
||||||
formatter.timeStyle = .short
|
|
||||||
return formatter
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
TrustedDevicesView()
|
|
||||||
.onAppear {
|
|
||||||
|
|
||||||
var cert1 = Sharing_Nearby_PublicCertificate()
|
|
||||||
cert1.secretID = Data([0x01, 0x02, 0x03, 0x04])
|
|
||||||
|
|
||||||
TrustStore.shared.addTrusted(certificate: cert1, device: RemoteDeviceInfo(name: "MacBook Pro", type: .computer))
|
|
||||||
|
|
||||||
|
|
||||||
var cert2 = Sharing_Nearby_PublicCertificate()
|
|
||||||
cert2.secretID = Data([0x0A, 0x0B, 0x0C, 0x0D])
|
|
||||||
|
|
||||||
TrustStore.shared.addTrusted(certificate: cert2, device: RemoteDeviceInfo(name: "iPhone", type: .phone))
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
struct LicensePage: View {
|
struct LicensePage: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -13,20 +13,35 @@ struct TutorialView: View {
|
|||||||
|
|
||||||
let tab: Tab
|
let tab: Tab
|
||||||
let openPlus: () -> Void
|
let openPlus: () -> Void
|
||||||
|
let openAppAdvertisementView: () -> Void
|
||||||
|
|
||||||
@State private var licenseWindow: NSWindow?
|
@State private var licenseWindow: NSWindow?
|
||||||
@ObservedObject var settings = Settings.shared
|
@ObservedObject var iapManager = IAPManager.sharedInstance
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
LargeAppIconView(title: tab.title) {
|
LargeAppIconView(title: tab.title) {
|
||||||
VStack {
|
VStack {
|
||||||
|
Group {
|
||||||
Text(tab.text.localized())
|
if tab == .troubleshooting {
|
||||||
.multilineTextAlignment(.leading)
|
FAQView(faqItems: [
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.init(question: "FaqNotVisibleOrConnectingQuestion", answer: "FaqNotVisibleOrConnectingAnswer"),
|
||||||
.padding()
|
.init(question: "FaqPhotoDateNotPreservedQuestion", answer: "FaqPhotoDateNotPreservedAnswer"),
|
||||||
.frame(width: 550)
|
.init(question: "FaqAndroidDeviceNotVisibleQuestion", answer: "FaqAndroidDeviceNotVisibleAnswer"),
|
||||||
|
.init(question: "FaqTrustedDevicesQuestion", answer: "FaqTrustedDevicesAnswer"),
|
||||||
|
.init(question: "MultipleFilesSendingQuestion", answer: "MultipleFilesSendingAnswer"),
|
||||||
|
.init(question: "FaqBugQuestion", answer: "FaqBugAnswer"),
|
||||||
|
])
|
||||||
|
.padding(.top)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Text(tab.text.localized())
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 550)
|
||||||
|
|
||||||
if tab == .receive {
|
if tab == .receive {
|
||||||
HStack(spacing: 30) {
|
HStack(spacing: 30) {
|
||||||
@@ -40,8 +55,9 @@ struct TutorialView: View {
|
|||||||
.opacity(0.5)
|
.opacity(0.5)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
if !settings.gotPlus {
|
#if !GITHUB
|
||||||
|
if !iapManager.plusVersionState {
|
||||||
Button {
|
Button {
|
||||||
openPlus()
|
openPlus()
|
||||||
} label: {
|
} label: {
|
||||||
@@ -52,10 +68,12 @@ struct TutorialView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Button {
|
Button {
|
||||||
Settings.shared.deleteAllUserDefaults()
|
IAPManager.sharedInstance.plusVersionState = false
|
||||||
|
Settings.sharedInstance.deleteAllUserDefaults()
|
||||||
} label: {
|
} label: {
|
||||||
Text("ResetAllSettings")
|
Text("ResetAllSettings")
|
||||||
.underline()
|
.underline()
|
||||||
@@ -63,13 +81,22 @@ struct TutorialView: View {
|
|||||||
.opacity(0.5)
|
.opacity(0.5)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
else if tab == .send {
|
else if tab == .send {
|
||||||
EnableExtensionView()
|
EnableExtensionView()
|
||||||
}
|
}
|
||||||
|
else if tab == .notificationSync {
|
||||||
|
Button {
|
||||||
|
openAppAdvertisementView()
|
||||||
|
} label: {
|
||||||
|
Text("DownloadMobileApp".localized())
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,8 +113,15 @@ struct EnableExtensionView: View {
|
|||||||
|
|
||||||
private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
|
private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
init() {
|
init(canShowIntialCheckmark: Bool = false) {
|
||||||
_isExtensionEnabled = State(initialValue: Self.isEnabled())
|
let isEnabled = Self.isEnabled()
|
||||||
|
|
||||||
|
_isExtensionEnabled = State(initialValue: isEnabled)
|
||||||
|
|
||||||
|
if canShowIntialCheckmark && isEnabled {
|
||||||
|
_showSuccessCheckmark = State(initialValue: true)
|
||||||
|
_animateCheckmark = State(initialValue: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -163,31 +197,6 @@ private struct SharingPickerPresenter: NSViewRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct LargeAppIconView<Content: View>: View {
|
|
||||||
|
|
||||||
let title: String
|
|
||||||
let bottomView: () -> Content
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ScrollView {
|
|
||||||
|
|
||||||
VStack {
|
|
||||||
AppIconView(hasPlusIcon: false, size: 120)
|
|
||||||
.padding(.top, 35)
|
|
||||||
|
|
||||||
Text(title.localized())
|
|
||||||
.font(.largeTitle)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
bottomView()
|
|
||||||
}
|
|
||||||
.frame(maxWidth: 500)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {})
|
WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {})
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ struct WelcomeScreen: View {
|
|||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
ExternalLinkLabel(label: "GetSupport", icon: "questionmark.circle") {
|
ExternalLinkLabel(label: "GetSupport", icon: "questionmark.circle") {
|
||||||
sendLoggingString()
|
LogExportPresenter.showUploadLogsAlert(openSupportMailWhenNotUploading: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
ExternalLinkLabel(label: "PrivacyPolicy", icon: "hand.raised") {
|
ExternalLinkLabel(label: "PrivacyPolicy", icon: "hand.raised") {
|
||||||
@@ -70,7 +70,6 @@ struct WelcomeScreen: View {
|
|||||||
.listStyle(SidebarListStyle())
|
.listStyle(SidebarListStyle())
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
.opacity(colorScheme == .light ? 1 : 0.5)
|
|
||||||
.edgesIgnoringSafeArea(.vertical)
|
.edgesIgnoringSafeArea(.vertical)
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -80,12 +79,16 @@ struct WelcomeScreen: View {
|
|||||||
SettingsView()
|
SettingsView()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
TutorialView(tab: selection, openPlus: openPlusScreen)
|
TutorialView(
|
||||||
.onAppear {
|
tab: selection,
|
||||||
if selection == .troubleshooting {
|
openPlus: openPlusScreen,
|
||||||
checkForNetworkIssues()
|
openAppAdvertisementView: openAppAdvertisementView
|
||||||
}
|
)
|
||||||
|
.onAppear {
|
||||||
|
if selection == .troubleshooting {
|
||||||
|
checkForNetworkIssues()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +100,7 @@ struct WelcomeScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func openPrivacyPolicy() {
|
func openPrivacyPolicy() {
|
||||||
if let url = URL(string: "https://leonboettger.com/quickdrop-privacy") {
|
if let url = URL(string: .privacyPolicyURL) {
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,6 +181,7 @@ struct ExternalLinkLabel: View {
|
|||||||
enum Tab: CaseIterable {
|
enum Tab: CaseIterable {
|
||||||
case receive
|
case receive
|
||||||
case send
|
case send
|
||||||
|
case notificationSync
|
||||||
case troubleshooting
|
case troubleshooting
|
||||||
case settings
|
case settings
|
||||||
|
|
||||||
@@ -187,8 +191,10 @@ enum Tab: CaseIterable {
|
|||||||
return "ReceiveFiles".localized()
|
return "ReceiveFiles".localized()
|
||||||
case .send:
|
case .send:
|
||||||
return "SendFiles".localized()
|
return "SendFiles".localized()
|
||||||
|
case .notificationSync:
|
||||||
|
return "NotificationSyncSidebar".localized()
|
||||||
case .troubleshooting:
|
case .troubleshooting:
|
||||||
return "DeviceNotShown".localized()
|
return "TroubleshootingAndFaq".localized()
|
||||||
case .settings:
|
case .settings:
|
||||||
return "Settings".localized()
|
return "Settings".localized()
|
||||||
}
|
}
|
||||||
@@ -196,8 +202,6 @@ enum Tab: CaseIterable {
|
|||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .troubleshooting:
|
|
||||||
return "Troubleshooting"
|
|
||||||
default:
|
default:
|
||||||
return sidebarTitle
|
return sidebarTitle
|
||||||
}
|
}
|
||||||
@@ -209,9 +213,9 @@ enum Tab: CaseIterable {
|
|||||||
"UserManualDescription"
|
"UserManualDescription"
|
||||||
case .send:
|
case .send:
|
||||||
"SendFilesDescription"
|
"SendFilesDescription"
|
||||||
case .troubleshooting:
|
case .notificationSync:
|
||||||
"TroubleshootingDescription"
|
"NotificationSyncManualDescription"
|
||||||
case .settings:
|
default:
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,6 +226,8 @@ enum Tab: CaseIterable {
|
|||||||
return "tray.and.arrow.down"
|
return "tray.and.arrow.down"
|
||||||
case .send:
|
case .send:
|
||||||
return "tray.and.arrow.up"
|
return "tray.and.arrow.up"
|
||||||
|
case .notificationSync:
|
||||||
|
return "bell"
|
||||||
case .troubleshooting:
|
case .troubleshooting:
|
||||||
return "exclamationmark.triangle"
|
return "exclamationmark.triangle"
|
||||||
case .settings:
|
case .settings:
|
||||||
@@ -7,11 +7,9 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import StoreKit
|
import StoreKit
|
||||||
import UserNotifications
|
|
||||||
import LUI
|
import LUI
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
import BezelNotification
|
import AppKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
class ReceiveModel: ObservableObject, InboundAppDelegate {
|
class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||||
@@ -19,14 +17,21 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
/// For each connection ID, store the last reported progress value
|
/// For each connection ID, store the last reported progress value
|
||||||
private var processes: [String: Double] = [:]
|
private var processes: [String: Double] = [:]
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@Published var progress: Double? = nil
|
@Published var progress: Double? = nil
|
||||||
@Published var toastActions: ToastViewAction? = nil
|
@Published var toastActions: ToastViewAction? = nil
|
||||||
|
@Published var consentState: ConsentToastState? = nil
|
||||||
|
@Published var activeDeviceName: String? = nil
|
||||||
|
@Published var toastIsVisible: Bool = false
|
||||||
|
@Published var toastDismissStyle: ToastDismissStyle = .slide
|
||||||
private var pendingReview = false
|
private var pendingReview = false
|
||||||
|
private var toastWindowPending = false
|
||||||
private var toastWindow: NSWindow?
|
private var toastWindow: NSWindow?
|
||||||
private var toastHosting: NSHostingView<QuickDropToastView>?
|
private var toastHosting: NSHostingView<QuickDropToastHostView>?
|
||||||
|
private var toastRevealTask: DispatchWorkItem?
|
||||||
|
private var toastCleanupTask: DispatchWorkItem?
|
||||||
private let monitor = AllowedWorkMonitor()
|
private let monitor = AllowedWorkMonitor()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
let controlPlusScreen: (Bool) -> Void
|
let controlPlusScreen: (Bool) -> Void
|
||||||
|
|
||||||
@@ -67,49 +72,105 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
AudioManager.playIncomingFileSound()
|
AudioManager.playIncomingFileSound()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
let mainMessage = transfer.getDescription(deviceName: device.name ?? "UnknownDevice".localized(), alreadyAccepted: false)
|
let mainMessage = transfer.getDescription(deviceName: device.name ?? "AndroidDevice".localized())
|
||||||
let pinCodeMessage = transfer.getPinCodeMessage()
|
let pinCodeMessage = transfer.getPinCodeMessage()
|
||||||
let transferID = transfer.id
|
let transferID = transfer.id
|
||||||
|
|
||||||
|
let primaryButtonAction = { (trustDevice: Bool) in
|
||||||
|
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, trustDevice: trustDevice)
|
||||||
|
}
|
||||||
|
|
||||||
|
let secondaryButtonAction = { NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: false, trustDevice: false) }
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if transfer.type == .notificationSync || transfer.type == .clipboardSync {
|
||||||
|
self.hideQuickDropToast()
|
||||||
|
self.toastActions = nil
|
||||||
|
self.progress = nil
|
||||||
|
self.consentState = nil
|
||||||
|
let senderName = device.name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let resolvedSenderName = (senderName?.isEmpty == false) ? senderName! : "AndroidDevice".localized()
|
||||||
|
self.activeDeviceName = resolvedSenderName
|
||||||
|
|
||||||
|
let pairingUseCase: PairingUseCase = transfer.type == .clipboardSync ? .clipboardSync : .notificationSync
|
||||||
|
let compareMessage = transfer.type == .clipboardSync
|
||||||
|
? "ClipboardSyncToastQrSubtitle".localized()
|
||||||
|
: "NotificationSyncToastQrSubtitle".localized()
|
||||||
|
self.consentState = ConsentToastState(
|
||||||
|
transferID: transferID,
|
||||||
|
pinCodeMessage: "",
|
||||||
|
message: compareMessage,
|
||||||
|
notificationSyncStage: .consent,
|
||||||
|
allowsTrust: false,
|
||||||
|
acceptAction: { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
withAnimation {
|
||||||
|
self.consentState = nil
|
||||||
|
}
|
||||||
|
guard let receiverFingerprint = PairingToken.receiverFingerprintHex() else {
|
||||||
|
log("[ReceiveModel] Missing local receiver fingerprint; cannot start notification sync pairing.")
|
||||||
|
secondaryButtonAction()
|
||||||
|
self.hideQuickDropToast(style: .fade)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let pairingToken = PairingToken.generate()
|
||||||
|
PairingQrSheetManager.shared.open(
|
||||||
|
token: pairingToken,
|
||||||
|
receiverFingerprint: receiverFingerprint,
|
||||||
|
deviceName: resolvedSenderName,
|
||||||
|
useCase: pairingUseCase
|
||||||
|
)
|
||||||
|
NearbyConnectionManager.shared.submitUserConsent(
|
||||||
|
transferID: transferID,
|
||||||
|
accept: true,
|
||||||
|
trustDevice: false,
|
||||||
|
pairingToken: pairingToken
|
||||||
|
)
|
||||||
|
self.hideQuickDropToast(style: .fade)
|
||||||
|
},
|
||||||
|
declineAction: { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.consentState = nil
|
||||||
|
secondaryButtonAction()
|
||||||
|
self.hideQuickDropToast(style: .fade)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.showQuickDropToast(for: transferID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.toastActions = nil
|
||||||
|
self.progress = nil
|
||||||
|
self.activeDeviceName = device.name ?? "AndroidDevice".localized()
|
||||||
|
self.consentState = ConsentToastState(
|
||||||
|
transferID: transferID,
|
||||||
|
pinCodeMessage: pinCodeMessage,
|
||||||
|
message: mainMessage,
|
||||||
|
notificationSyncStage: nil,
|
||||||
|
allowsTrust: transfer.allowsToBeAddedAsTrustedDevice,
|
||||||
|
acceptAction: { [weak self] trustDevice in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
withAnimation {
|
||||||
|
self?.consentState = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primaryButtonAction(trustDevice)
|
||||||
|
},
|
||||||
|
declineAction: { [weak self] in
|
||||||
|
self?.consentState = nil
|
||||||
|
secondaryButtonAction()
|
||||||
|
self?.hideQuickDropToast()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.showQuickDropToast(for: transferID)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
|
||||||
let title = "QuickDrop - \(pinCodeMessage)"
|
let title = "QuickDrop - \(pinCodeMessage)"
|
||||||
let primaryButtonTitle = "Accept".localized()
|
let primaryButtonTitle = "Accept".localized()
|
||||||
let primaryButtonAction = { (trustDevice: Bool) in
|
|
||||||
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, trustDevice: trustDevice)
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
if transfer.type == .text {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
BezelNotification.show(messageText: "InsertedIntoClipboard".localized(), icon: .clipboard)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
let secondaryButtonTitle = "Decline".localized()
|
let secondaryButtonTitle = "Decline".localized()
|
||||||
let secondaryButtonAction = { NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: false, trustDevice: false) }
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.alertStyle = .informational
|
|
||||||
|
|
||||||
alert.messageText = title
|
|
||||||
alert.informativeText = mainMessage
|
|
||||||
alert.addButton(withTitle: primaryButtonTitle)
|
|
||||||
alert.addButton(withTitle: secondaryButtonTitle)
|
|
||||||
|
|
||||||
if transfer.allowsToBeAddedAsTrustedDevice {
|
|
||||||
alert.showsSuppressionButton = true
|
|
||||||
alert.suppressionButton?.title = "AutoAcceptFromThisDevice".localized()
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = alert.runModal()
|
|
||||||
|
|
||||||
if result == .alertFirstButtonReturn {
|
|
||||||
primaryButtonAction(alert.suppressionButton?.state == .on)
|
|
||||||
} else if result == .alertSecondButtonReturn {
|
|
||||||
secondaryButtonAction()
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// iOS
|
// iOS
|
||||||
let alwaysAcceptLabel = transfer.allowsToBeAddedAsTrustedDevice ? "AlwaysAccept".localized() : nil
|
let alwaysAcceptLabel = transfer.allowsToBeAddedAsTrustedDevice ? "AlwaysAccept".localized() : nil
|
||||||
ProgressAlert.shared.askForUserPermission(title: title, message: mainMessage, acceptLabel: primaryButtonTitle, acceptAlwaysLabel: alwaysAcceptLabel, rejectLabel: secondaryButtonTitle) { accepted in
|
ProgressAlert.shared.askForUserPermission(title: title, message: mainMessage, acceptLabel: primaryButtonTitle, acceptAlwaysLabel: alwaysAcceptLabel, rejectLabel: secondaryButtonTitle) { accepted in
|
||||||
@@ -128,34 +189,24 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
|
|
||||||
|
|
||||||
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo) {
|
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo) {
|
||||||
|
#if os(macOS)
|
||||||
|
if transfer.isClipboardSync {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
let mainMessage = transfer.getDescription(deviceName: device.name ?? "UnknownDevice".localized(), alreadyAccepted: true)
|
let mainMessage = transfer.getDescription(deviceName: device.name ?? "AndroidDevice".localized())
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
AudioManager.playIncomingFileSound()
|
AudioManager.playIncomingFileSound()
|
||||||
|
DispatchQueue.main.async {
|
||||||
// If file -> no notification, as there is the QuickDrop progress toast
|
self.toastActions = nil
|
||||||
if transfer.type != .file {
|
self.progress = nil
|
||||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in
|
self.activeDeviceName = device.name ?? "AndroidDevice".localized()
|
||||||
if granted {
|
self.consentState = nil
|
||||||
log("[ReceiveModel] User granted notification permissions")
|
self.showQuickDropToast(for: transfer.id)
|
||||||
|
|
||||||
let notificationContent = UNMutableNotificationContent()
|
|
||||||
notificationContent.title = "QuickDrop"
|
|
||||||
notificationContent.body = mainMessage
|
|
||||||
notificationContent.sound = nil
|
|
||||||
let notificationId = UUID().uuidString
|
|
||||||
|
|
||||||
let notificationReq = UNNotificationRequest(identifier: notificationId, content: notificationContent, trigger: nil)
|
|
||||||
UNUserNotificationCenter.current().add(notificationReq)
|
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
|
|
||||||
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// If text is received, nothing is shown, as it is directly inserted into clipboard automatically
|
// If text is received, nothing is shown, as it is directly inserted into clipboard automatically
|
||||||
@@ -188,19 +239,37 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func pairingSetupConfirmed() {
|
||||||
|
#if os(macOS)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if self.consentState?.notificationSyncStage != nil {
|
||||||
|
self.hideQuickDropToast(style: .fade)
|
||||||
|
}
|
||||||
|
PairingQrSheetManager.shared.close()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, error: (any Error)?) {
|
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, wasClipboardSync: Bool, error: (any Error)?) {
|
||||||
|
|
||||||
processes.removeValue(forKey: connectionID)
|
processes.removeValue(forKey: connectionID)
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
let wasPendingConsent = self.consentState?.transferID == connectionID
|
||||||
if self.processes.isEmpty {
|
if self.processes.isEmpty {
|
||||||
self.progress = nil
|
self.progress = nil
|
||||||
}
|
}
|
||||||
|
if wasPendingConsent {
|
||||||
|
self.consentState = nil
|
||||||
|
self.toastActions = nil
|
||||||
|
self.hideQuickDropToast()
|
||||||
|
}
|
||||||
|
finishMacTermination(device: device, savedFiles: savedFiles, error: error, wasPendingConsent: wasPendingConsent)
|
||||||
}
|
}
|
||||||
finish()
|
|
||||||
#else
|
#else
|
||||||
if self.processes.isEmpty {
|
if self.processes.isEmpty {
|
||||||
ProgressAlert.shared.updateProgress(nil, onCancel: {}) {
|
ProgressAlert.shared.updateProgress(nil, onCancel: {}) {
|
||||||
@@ -247,20 +316,31 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
let currentCount = Settings.shared.incomingTransmissionCount
|
let currentCount = Settings.sharedInstance.incomingTransmissionCount
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|
||||||
|
let completionKey: String
|
||||||
|
let autoHideDelay: TimeInterval
|
||||||
|
|
||||||
|
if !savedFiles.isEmpty {
|
||||||
|
completionKey = "Saved"
|
||||||
|
autoHideDelay = 10
|
||||||
|
} else if wasPlainTextTransfer {
|
||||||
|
completionKey = "CopiedToClipboard"
|
||||||
|
autoHideDelay = 10
|
||||||
|
} else {
|
||||||
|
completionKey = "URLOpened"
|
||||||
|
autoHideDelay = 4
|
||||||
|
}
|
||||||
|
|
||||||
if currentCount == 0 {
|
if currentCount == 0 {
|
||||||
pendingReview = true
|
pendingReview = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let openFilesAction = {
|
let openFilesAction: (() -> ())? = savedFiles.isEmpty ? nil : {
|
||||||
if !savedFiles.isEmpty {
|
log("[SaveFilesManager] Opening \(savedFiles.count) file(s) in Finder.")
|
||||||
log("[SaveFilesManager] Opening \(savedFiles.count) file(s) in Finder.")
|
NSWorkspace.shared.activateFileViewerSelecting(savedFiles)
|
||||||
NSWorkspace.shared.activateFileViewerSelecting(savedFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,28 +366,121 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
} : nil
|
} : nil
|
||||||
|
|
||||||
let closeToastAction = {
|
let closeToastAction = {
|
||||||
|
self.hideQuickDropToast(style: .fade)
|
||||||
|
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
||||||
|
}
|
||||||
|
let autoHideAction = {
|
||||||
self.hideQuickDropToast()
|
self.hideQuickDropToast()
|
||||||
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
self.toastActions = ToastViewAction(openFilesAction: openFilesAction, importPhotosAction: importPhotosAction, closeToastAction: closeToastAction)
|
self.toastActions = ToastViewAction(completionMessageKey: completionKey, autoHideDelay: autoHideDelay, openFilesAction: openFilesAction, importPhotosAction: importPhotosAction, closeToastAction: closeToastAction, autoHideAction: autoHideAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show received files if wanted
|
// Show received files if wanted
|
||||||
if Settings.shared.openFinderAfterReceiving {
|
if Settings.sharedInstance.openFinderAfterReceiving {
|
||||||
self.toastActions?.openFilesAction()
|
self.toastActions?.openFilesAction?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Settings.shared.incomingTransmissionCount = currentCount + 1
|
Settings.sharedInstance.incomingTransmissionCount = currentCount + 1
|
||||||
log("[ReceiveModel] Successful transmission. Current count: \(currentCount)")
|
log("[ReceiveModel] Successful transmission. Current count: \(currentCount)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
func finishMacTermination(device: RemoteDeviceInfo?, savedFiles: [URL], error: (any Error)?, wasPendingConsent: Bool) {
|
||||||
|
if wasPendingConsent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if wasClipboardSync && error == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = error {
|
||||||
|
if let name = device?.name {
|
||||||
|
self.hideQuickDropToast()
|
||||||
|
|
||||||
|
controlPlusScreen(false)
|
||||||
|
|
||||||
|
errorVibration()
|
||||||
|
ErrorAlertHandler.shared.showErrorAlert(for: name, error: error)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentCount = Settings.sharedInstance.incomingTransmissionCount
|
||||||
|
let completionKey: String
|
||||||
|
let autoHideDelay: TimeInterval
|
||||||
|
|
||||||
|
if !savedFiles.isEmpty {
|
||||||
|
completionKey = "Saved"
|
||||||
|
autoHideDelay = 10
|
||||||
|
} else if wasPlainTextTransfer {
|
||||||
|
completionKey = "CopiedToClipboard"
|
||||||
|
autoHideDelay = 10
|
||||||
|
} else {
|
||||||
|
completionKey = "URLOpened"
|
||||||
|
autoHideDelay = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentCount == 0 {
|
||||||
|
pendingReview = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let openFilesAction: (() -> ())? = savedFiles.isEmpty ? nil : {
|
||||||
|
log("[SaveFilesManager] Opening \(savedFiles.count) file(s) in Finder.")
|
||||||
|
NSWorkspace.shared.activateFileViewerSelecting(savedFiles)
|
||||||
|
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasPhotoOrVideos = PhotoManager.hasPhotosOrVideos(at: savedFiles)
|
||||||
|
let importPhotosAction: (()->())? = hasPhotoOrVideos ? {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await PhotoManager.saveMediaToPhotoLibrary(from: savedFiles)
|
||||||
|
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.alertStyle = .critical
|
||||||
|
alert.messageText = "CouldNotSaveMediaToPhotoLibrary".localized()
|
||||||
|
alert.informativeText = error.localizedDescription
|
||||||
|
alert.addButton(withTitle: "CloseAlert".localized())
|
||||||
|
|
||||||
|
alert.runModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : nil
|
||||||
|
|
||||||
|
let closeToastAction = {
|
||||||
|
self.hideQuickDropToast(style: .fade)
|
||||||
|
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
||||||
|
}
|
||||||
|
let autoHideAction = {
|
||||||
|
self.hideQuickDropToast()
|
||||||
|
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
withAnimation {
|
||||||
|
self.toastActions = ToastViewAction(completionMessageKey: completionKey, autoHideDelay: autoHideDelay, openFilesAction: openFilesAction, importPhotosAction: importPhotosAction, closeToastAction: closeToastAction, autoHideAction: autoHideAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show received files if wanted
|
||||||
|
if Settings.sharedInstance.openFinderAfterReceiving {
|
||||||
|
self.toastActions?.openFilesAction?()
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.sharedInstance.incomingTransmissionCount = currentCount + 1
|
||||||
|
log("[ReceiveModel] Successful transmission. Current count: \(currentCount)")
|
||||||
|
}
|
||||||
|
|
||||||
func showReviewIfAppropriate(currentTransmissionCount: Int) {
|
func showReviewIfAppropriate(currentTransmissionCount: Int) {
|
||||||
if pendingReview {
|
if pendingReview {
|
||||||
// Dont request review again
|
// Dont request review again
|
||||||
@@ -324,6 +497,12 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
|
|
||||||
func showPlusScreen() {
|
func showPlusScreen() {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
#if os(macOS)
|
||||||
|
self.consentState = nil
|
||||||
|
self.toastActions = nil
|
||||||
|
self.progress = nil
|
||||||
|
self.hideQuickDropToast()
|
||||||
|
#endif
|
||||||
self.controlPlusScreen(true)
|
self.controlPlusScreen(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,31 +511,88 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
// MARK: - macOS Progress Overlay
|
// MARK: - macOS Progress Overlay
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
private func scheduleToastReveal(for window: NSWindow) {
|
||||||
|
toastRevealTask?.cancel()
|
||||||
|
toastRevealTask = nil
|
||||||
|
|
||||||
|
let revealTask = DispatchWorkItem { [weak self, weak window] in
|
||||||
|
guard let self, let window else { return }
|
||||||
|
guard self.toastWindow === window else { return }
|
||||||
|
self.toastIsVisible = true
|
||||||
|
self.toastRevealTask = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
toastRevealTask = revealTask
|
||||||
|
// Reveal on the next run-loop tick to avoid pop-in without timer jitter.
|
||||||
|
DispatchQueue.main.async(execute: revealTask)
|
||||||
|
}
|
||||||
|
|
||||||
func showQuickDropToast(for connectionID: String) {
|
func showQuickDropToast(for connectionID: String) {
|
||||||
guard toastWindow == nil else { return }
|
if !Thread.isMainThread {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.showQuickDropToast(for: connectionID)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let contentView = QuickDropToastView(
|
// If a previous hide is still waiting for cleanup, keep the toast alive for this new request.
|
||||||
|
toastCleanupTask?.cancel()
|
||||||
|
toastCleanupTask = nil
|
||||||
|
toastRevealTask?.cancel()
|
||||||
|
toastRevealTask = nil
|
||||||
|
|
||||||
|
if let window = toastWindow {
|
||||||
|
toastDismissStyle = .slide
|
||||||
|
|
||||||
|
if !toastIsVisible {
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
scheduleToastReveal(for: window)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !toastWindowPending else { return }
|
||||||
|
toastWindowPending = true
|
||||||
|
|
||||||
|
guard toastWindow == nil else {
|
||||||
|
toastWindowPending = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toastIsVisible = false
|
||||||
|
toastDismissStyle = .slide
|
||||||
|
|
||||||
|
let contentView = QuickDropToastHostView(
|
||||||
receiveModel: self,
|
receiveModel: self,
|
||||||
onCancel: {
|
onCancel: { [weak self] in
|
||||||
NearbyConnectionManager.shared.cancelTransfer(id: connectionID)
|
guard let self else { return }
|
||||||
|
NearbyConnectionManager.shared.cancelAllIncomingConnections()
|
||||||
|
|
||||||
|
self.processes.removeAll()
|
||||||
|
self.progress = nil
|
||||||
|
self.toastActions = nil
|
||||||
|
self.consentState = nil
|
||||||
|
self.hideQuickDropToast(style: .fade)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
let hostingView = NSHostingView(rootView: contentView)
|
let hostingView = NSHostingView(rootView: contentView)
|
||||||
hostingView.frame.size = toastViewSize
|
|
||||||
|
|
||||||
// Prefer the screen under the mouse, then the key window’s screen, then main
|
// Prefer the screen under the mouse, then the key window’s screen, then main
|
||||||
let screen = NSScreen.screens.first { $0.frame.contains(NSEvent.mouseLocation) }
|
let screen = NSScreen.screens.first { $0.frame.contains(NSEvent.mouseLocation) }
|
||||||
?? NSApp.keyWindow?.screen
|
?? NSApp.keyWindow?.screen
|
||||||
?? NSScreen.main
|
?? NSScreen.main
|
||||||
|
|
||||||
guard let screen else { return }
|
guard let screen else {
|
||||||
|
toastWindowPending = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let frame = screen.frame
|
let frame = screen.frame
|
||||||
let visible = screen.visibleFrame
|
let visible = screen.visibleFrame
|
||||||
|
|
||||||
let marginX: CGFloat = 24
|
let marginX: CGFloat = 20
|
||||||
let marginFromTop: CGFloat = 30
|
let marginFromTop: CGFloat = 20
|
||||||
|
|
||||||
// If the menu bar is auto-hidden, visible.maxY == frame.maxY; subtract the status bar thickness to stay clear
|
// If the menu bar is auto-hidden, visible.maxY == frame.maxY; subtract the status bar thickness to stay clear
|
||||||
let menuBarHidden = abs(frame.maxY - visible.maxY) < 0.5
|
let menuBarHidden = abs(frame.maxY - visible.maxY) < 0.5
|
||||||
@@ -364,61 +600,130 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
|
|
||||||
let targetX = visible.maxX - toastViewSize.width - marginX
|
let targetX = visible.maxX - toastViewSize.width - marginX
|
||||||
let targetY = visible.maxY - toastViewSize.height - marginFromTop - extraTopInset
|
let targetY = visible.maxY - toastViewSize.height - marginFromTop - extraTopInset
|
||||||
|
let windowWidth = visible.maxX - targetX
|
||||||
|
let targetFrame = CGRect(x: targetX, y: targetY, width: windowWidth, height: toastViewSize.height)
|
||||||
|
|
||||||
|
// Expand the borderless window so SwiftUI shadow can render without clipping.
|
||||||
|
// Keep the toast anchored at the same screen position by offsetting hostingView
|
||||||
|
// inside a clear container view.
|
||||||
|
let shadowInsetLeft: CGFloat = 22
|
||||||
|
let shadowInsetTop: CGFloat = 18
|
||||||
|
let shadowInsetBottom: CGFloat = 24
|
||||||
|
let windowFrame = CGRect(
|
||||||
|
x: targetFrame.minX - shadowInsetLeft,
|
||||||
|
y: targetFrame.minY - shadowInsetBottom,
|
||||||
|
width: targetFrame.width + shadowInsetLeft,
|
||||||
|
height: targetFrame.height + shadowInsetTop + shadowInsetBottom
|
||||||
|
)
|
||||||
|
|
||||||
|
let containerView = NSView(frame: CGRect(origin: .zero, size: windowFrame.size))
|
||||||
|
containerView.wantsLayer = true
|
||||||
|
containerView.layer?.backgroundColor = NSColor.clear.cgColor
|
||||||
|
|
||||||
|
hostingView.frame = CGRect(
|
||||||
|
x: shadowInsetLeft,
|
||||||
|
y: shadowInsetBottom,
|
||||||
|
width: targetFrame.width,
|
||||||
|
height: targetFrame.height
|
||||||
|
)
|
||||||
|
containerView.addSubview(hostingView)
|
||||||
|
|
||||||
// Start slightly above the final spot for a smooth slide-in
|
let window = NSWindow(contentRect: windowFrame,
|
||||||
let startY = min(frame.maxY + 10, targetY + 10)
|
|
||||||
let startFrame = CGRect(x: targetX, y: startY, width: toastViewSize.width, height: toastViewSize.height)
|
|
||||||
|
|
||||||
let window = NSWindow(contentRect: startFrame,
|
|
||||||
styleMask: [.borderless],
|
styleMask: [.borderless],
|
||||||
backing: .buffered,
|
backing: .buffered,
|
||||||
defer: false)
|
defer: false)
|
||||||
window.isOpaque = false
|
window.isOpaque = false
|
||||||
window.backgroundColor = .clear
|
window.backgroundColor = .clear
|
||||||
window.hasShadow = true
|
// Use SwiftUI shadow on the toast view itself. On macOS 12, window
|
||||||
|
// shadow can reveal a rectangular artifact in transparent trailing space.
|
||||||
|
window.hasShadow = false
|
||||||
window.level = .statusBar
|
window.level = .statusBar
|
||||||
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||||
window.contentView = hostingView
|
window.contentView = containerView
|
||||||
window.ignoresMouseEvents = false
|
window.ignoresMouseEvents = false
|
||||||
window.alphaValue = 0
|
window.alphaValue = 1
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
|
||||||
self.toastWindow = window
|
toastWindow = window
|
||||||
self.toastHosting = hostingView
|
toastHosting = hostingView
|
||||||
|
toastWindowPending = false
|
||||||
NSAnimationContext.runAnimationGroup { ctx in
|
scheduleToastReveal(for: window)
|
||||||
ctx.duration = 0.35
|
|
||||||
ctx.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
||||||
window.animator().setFrameOrigin(CGPoint(x: targetX, y: targetY))
|
|
||||||
window.animator().alphaValue = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func hideQuickDropToast() {
|
func hideQuickDropToast(style: ToastDismissStyle = .fade) {
|
||||||
guard let window = toastWindow,
|
if !Thread.isMainThread {
|
||||||
let screen = NSScreen.main else { return }
|
DispatchQueue.main.async {
|
||||||
|
self.hideQuickDropToast(style: style)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let offscreenY = screen.visibleFrame.origin.y + screen.visibleFrame.height + 20
|
toastCleanupTask?.cancel()
|
||||||
|
toastCleanupTask = nil
|
||||||
|
toastRevealTask?.cancel()
|
||||||
|
toastRevealTask = nil
|
||||||
|
|
||||||
NSAnimationContext.runAnimationGroup({ ctx in
|
guard let window = toastWindow else {
|
||||||
ctx.duration = 0.35
|
self.toastWindow = nil
|
||||||
ctx.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
self.toastHosting = nil
|
||||||
window.animator().setFrameOrigin(CGPoint(x: window.frame.origin.x, y: offscreenY))
|
self.toastActions = nil
|
||||||
window.animator().alphaValue = 0
|
self.consentState = nil
|
||||||
}, completionHandler: {
|
self.toastWindowPending = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.toastDismissStyle = style
|
||||||
|
self.toastIsVisible = false
|
||||||
|
|
||||||
|
let cleanupDelay: TimeInterval = (style == .slide) ? 0.5 : 0.3
|
||||||
|
let cleanupTask = DispatchWorkItem { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
window.orderOut(nil)
|
window.orderOut(nil)
|
||||||
self.toastWindow = nil
|
self.toastWindow = nil
|
||||||
self.toastHosting = nil
|
self.toastHosting = nil
|
||||||
self.toastActions = nil
|
self.toastActions = nil
|
||||||
})
|
self.consentState = nil
|
||||||
|
self.activeDeviceName = nil
|
||||||
|
self.toastWindowPending = false
|
||||||
|
self.toastCleanupTask = nil
|
||||||
|
self.toastRevealTask = nil
|
||||||
|
}
|
||||||
|
toastCleanupTask = cleanupTask
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + cleanupDelay, execute: cleanupTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct ToastViewAction {
|
struct ToastViewAction {
|
||||||
let openFilesAction: () -> ()
|
let completionMessageKey: String
|
||||||
|
let autoHideDelay: TimeInterval
|
||||||
|
let openFilesAction: (() -> ())?
|
||||||
let importPhotosAction: (() -> ())?
|
let importPhotosAction: (() -> ())?
|
||||||
let closeToastAction: () -> ()
|
let closeToastAction: () -> ()
|
||||||
|
let autoHideAction: () -> ()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum ToastDismissStyle {
|
||||||
|
case slide
|
||||||
|
case fade
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct ConsentToastState {
|
||||||
|
enum NotificationSyncStage {
|
||||||
|
case consent
|
||||||
|
case pin
|
||||||
|
}
|
||||||
|
|
||||||
|
let transferID: String
|
||||||
|
let pinCodeMessage: String
|
||||||
|
let message: String
|
||||||
|
let notificationSyncStage: NotificationSyncStage?
|
||||||
|
let allowsTrust: Bool
|
||||||
|
let acceptAction: (Bool) -> ()
|
||||||
|
let declineAction: () -> ()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,13 @@ class SendModel: ObservableObject, OutboundAppDelegate {
|
|||||||
selectedDevice = device
|
selectedDevice = device
|
||||||
|
|
||||||
runAfter(seconds: 0.3) {
|
runAfter(seconds: 0.3) {
|
||||||
NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: self.urls, textToSend: self.textToSend)
|
NearbyConnectionManager.shared.startOutgoingTransfer(
|
||||||
|
deviceID: device.id!,
|
||||||
|
delegate: self,
|
||||||
|
urls: self.urls,
|
||||||
|
textToSend: self.textToSend,
|
||||||
|
receiverAuthenticationPolicy: .none
|
||||||
|
)
|
||||||
self.progressState = "Connecting".localized()
|
self.progressState = "Connecting".localized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,19 @@
|
|||||||
"layers" : [
|
"layers" : [
|
||||||
{
|
{
|
||||||
"image-name" : "QuickDropArrows.png",
|
"image-name" : "QuickDropArrows.png",
|
||||||
"name" : "QuickDropArrows"
|
"name" : "QuickDropArrows",
|
||||||
|
"position-specializations" : [
|
||||||
|
{
|
||||||
|
"idiom" : "square",
|
||||||
|
"value" : {
|
||||||
|
"scale" : 1,
|
||||||
|
"translation-in-points" : [
|
||||||
|
0,
|
||||||
|
-5
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image-name" : "QuickDropCircle.png",
|
"image-name" : "QuickDropCircle.png",
|
||||||
@@ -24,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
"translucency" : {
|
"translucency" : {
|
||||||
"enabled" : true,
|
"enabled" : true,
|
||||||
"value" : 0.16
|
"value" : 0.52
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
//
|
|
||||||
// Configuration.swift
|
|
||||||
// QuickDrop
|
|
||||||
//
|
|
||||||
// Created by Leon Böttger on 15.08.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import LUI
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
let configuration = LUIConfiguration(
|
|
||||||
appName: "QuickDrop",
|
|
||||||
|
|
||||||
plusVersionInfo: GetPlusViewInformation(lifetimeID: "plusversion", monthlyID: "quickdrop.monthly", yearlyID: "quickdrop.yearly", plusViewHeader: isMac() ? "plusview_title" : nil, description: isMac() ? "plusview_description" : "plusview_description_phone", onBoughtPlusVersionAlertMessage: "plusview_success_description", plusVersionFeatures: [], settingsLabelColor: .orange, settingsFooter: "plusview_footer_phone"),
|
|
||||||
|
|
||||||
setPlusVersion: { result in
|
|
||||||
#if !GITHUB
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
Settings.shared.gotPlus = result
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
},
|
|
||||||
|
|
||||||
hasRoundedFont: false,
|
|
||||||
|
|
||||||
switchedToIAPafterBundleVersion: AppVersionBundle(appVersion: "1.2", buildVersion: 0, legacyUserDefaultsKey: Settings.UserDefaultsKeys.plusVersionLegacy.rawValue),
|
|
||||||
|
|
||||||
copyrightInformation: licenseText,
|
|
||||||
|
|
||||||
loggingUrl: AppGroup.appGroupDirectory,
|
|
||||||
|
|
||||||
introductionViewInformation: "QuickDropWelcomeDescription",
|
|
||||||
|
|
||||||
usesAutomaticReviewPrompt: false,
|
|
||||||
|
|
||||||
usesPagedIntroduction: true,
|
|
||||||
|
|
||||||
supportsSplitView: false,
|
|
||||||
|
|
||||||
supportsLiquidGlass: true,
|
|
||||||
|
|
||||||
supportsPortraitSplitView: false,
|
|
||||||
|
|
||||||
supportsDarkModeShadow: false
|
|
||||||
)
|
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "new",
|
||||||
"value" : ""
|
"value" : "Copyright © 2025-2026 Leon Böttger"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,9 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>group.com.leonboettger.neardrop</string>
|
<string>group.com.leonboettger.neardrop</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -84,7 +84,13 @@ class SendModel: ObservableObject, OutboundAppDelegate {
|
|||||||
progressValue = 0
|
progressValue = 0
|
||||||
progressState = "Connecting".localized()
|
progressState = "Connecting".localized()
|
||||||
selectedDevice = device
|
selectedDevice = device
|
||||||
NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: urls, textToSend: textToSend)
|
NearbyConnectionManager.shared.startOutgoingTransfer(
|
||||||
|
deviceID: device.id!,
|
||||||
|
delegate: self,
|
||||||
|
urls: urls,
|
||||||
|
textToSend: textToSend,
|
||||||
|
receiverAuthenticationPolicy: .none
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ struct ChangeDeviceNameAlertModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func changeDeviceNameAlert(
|
func changeDeviceNameAlert(
|
||||||
isPresented: Binding<Bool>,
|
isPresented: Binding<Bool>,
|
||||||
@@ -12,14 +12,14 @@ import StoreKit
|
|||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
@ObservedObject var luiSettings = LUISettings.sharedInstance
|
@ObservedObject var luiSettings = LUISettings.sharedInstance
|
||||||
@ObservedObject var settings = Settings.shared
|
@ObservedObject var iapManager = IAPManager.sharedInstance
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
AppRootView(isPlus: $settings.gotPlus, phoneView: {
|
AppRootView(phoneView: {
|
||||||
DeviceListView()
|
DeviceListView()
|
||||||
.environment(\.sheetActive, isShareExtension())
|
.environment(\.sheetActive, isShareExtension())
|
||||||
}, settingsView: {
|
}, settingsView: {
|
||||||
CustomSection(header: settings.gotPlus ? "" : "General", footer: "TrustedDevicesFooterShort") {
|
CustomSection(header: iapManager.plusVersionState ? "" : "General", footer: "TrustedDevicesFooterShort") {
|
||||||
LUIButton {
|
LUIButton {
|
||||||
FileManager.default.openDocumentFolder()
|
FileManager.default.openDocumentFolder()
|
||||||
} label: {
|
} label: {
|
||||||
@@ -47,22 +47,25 @@ struct DeviceListView: View {
|
|||||||
@StateObject var receiveModel = ReceiveModel(controlPlusScreen: { show in
|
@StateObject var receiveModel = ReceiveModel(controlPlusScreen: { show in
|
||||||
log("[DeviceListView] Setting showPlusScreen to \(show)")
|
log("[DeviceListView] Setting showPlusScreen to \(show)")
|
||||||
errorVibration()
|
errorVibration()
|
||||||
DeviceListViewModel.sharedInstance.showPlusScreen = show
|
if show {
|
||||||
|
presentPaywall()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
OverlayCoverStack.shared.dismissTop(animated: true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ObservedObject var model = DeviceListViewModel.sharedInstance
|
|
||||||
|
|
||||||
@Environment(\.scenePhase) var scenePhase
|
@Environment(\.scenePhase) var scenePhase
|
||||||
@Environment(\.sheetActive) var sheetActive
|
@Environment(\.sheetActive) var sheetActive
|
||||||
@ObservedObject var nearbyConnectionManager = NearbyConnectionManager.shared
|
@ObservedObject var nearbyConnectionManager = NearbyConnectionManager.shared
|
||||||
@ObservedObject var settings = Settings.shared
|
@ObservedObject var settings = Settings.sharedInstance
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork
|
let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork
|
||||||
|
|
||||||
BottomBarView(header: "QuickDrop", navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) {
|
BottomBarView(header: "QuickDrop", ignoresKeyboard: true, navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
||||||
let attachments = nearbyConnectionManager.attachments
|
let attachments = nearbyConnectionManager.attachments
|
||||||
@@ -167,9 +170,8 @@ struct DeviceListView: View {
|
|||||||
.font(.system(size: 14))
|
.font(.system(size: 14))
|
||||||
.foregroundColor(Color.primary.opacity(0.6))
|
.foregroundColor(Color.primary.opacity(0.6))
|
||||||
|
|
||||||
LoadingIndicator()
|
LoadingIndicator(size: 12)
|
||||||
.opacity(0.75)
|
.opacity(0.75)
|
||||||
.frame(width: 12, height: 12)
|
|
||||||
}
|
}
|
||||||
.padding(.vertical, 16)
|
.padding(.vertical, 16)
|
||||||
}
|
}
|
||||||
@@ -203,7 +205,7 @@ struct DeviceListView: View {
|
|||||||
#if !EXTENSION
|
#if !EXTENSION
|
||||||
NearbyConnectionManager.shared.becomeVisible()
|
NearbyConnectionManager.shared.becomeVisible()
|
||||||
|
|
||||||
if Settings.shared.incomingTransmissionCount > 0 {
|
if Settings.sharedInstance.incomingTransmissionCount > 0 {
|
||||||
runAfter(seconds: 0.3) {
|
runAfter(seconds: 0.3) {
|
||||||
requestReviewOnce()
|
requestReviewOnce()
|
||||||
}
|
}
|
||||||
@@ -220,9 +222,6 @@ struct DeviceListView: View {
|
|||||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if !EXTENSION
|
|
||||||
.modifier(PlusVersionSheetModifier(sheetActive: $model.showPlusScreen, isPlusVersion: $settings.gotPlus))
|
|
||||||
#endif
|
|
||||||
.animation(.smooth, value: nearbyConnectionManager.hasLocalNetworkPermission)
|
.animation(.smooth, value: nearbyConnectionManager.hasLocalNetworkPermission)
|
||||||
.animation(.smooth, value: nearbyConnectionManager.isConnectedToLocalNetwork)
|
.animation(.smooth, value: nearbyConnectionManager.isConnectedToLocalNetwork)
|
||||||
.navigationBarItems(trailing: ZStack {
|
.navigationBarItems(trailing: ZStack {
|
||||||
@@ -237,13 +236,6 @@ struct DeviceListView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final class DeviceListViewModel: SharedInstance {
|
|
||||||
static var sharedInstance = DeviceListViewModel()
|
|
||||||
|
|
||||||
@Published var showPlusScreen = false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct DeviceFilePickerButton: View {
|
struct DeviceFilePickerButton: View {
|
||||||
|
|
||||||
let device: RemoteDeviceInfo
|
let device: RemoteDeviceInfo
|
||||||
@@ -277,7 +269,7 @@ struct DeviceButtonLabel: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
|
|
||||||
let name = device.name ?? "UnknownDevice".localized()
|
let name = device.name ?? "AndroidDevice".localized()
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
@@ -296,14 +288,25 @@ struct DeviceButtonLabel: View {
|
|||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(name)
|
HStack(spacing: 5) {
|
||||||
.fontWeight(.medium)
|
Text(name)
|
||||||
.foregroundColor(Color.primary)
|
.foregroundColor(.mainColor)
|
||||||
|
|
||||||
|
if device.isQuickDropPeer {
|
||||||
|
Image(.menuBarIcon)
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 14, height: 14)
|
||||||
|
.foregroundColor(Color.primary)
|
||||||
|
.opacity(0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text(isSelected ? progress ?? "..." : "Available".localized())
|
Text(isSelected ? progress ?? "..." : "Available".localized())
|
||||||
.font(.system(size: 12))
|
.foregroundColor(.mainColor.opacity(0.6))
|
||||||
.foregroundColor(Color.primary.opacity(0.6))
|
.monospacedDigit()
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public struct LocalNetworkPermissionView: View {
|
|||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
|
|
||||||
PermissionView(title: "introduction_local_network_access", symbol: "", subtitle: "introduction_local_network_access_description", permissionAction: {
|
PermissionView(title: "introduction_local_network_access", watchSymbol: "", subtitle: "introduction_local_network_access_description".localized(with: "introduction_continue".localized()), permissionAction: {
|
||||||
|
|
||||||
if requestedLocalNetworkAccess {
|
if requestedLocalNetworkAccess {
|
||||||
// Something went wrong, user pressed again on continue. In this case, just continue with setup
|
// Something went wrong, user pressed again on continue. In this case, just continue with setup
|
||||||
@@ -27,15 +27,12 @@ public struct LocalNetworkPermissionView: View {
|
|||||||
let manager = NearbyConnectionManager.shared
|
let manager = NearbyConnectionManager.shared
|
||||||
manager.startDeviceDiscovery()
|
manager.startDeviceDiscovery()
|
||||||
}
|
}
|
||||||
}, canSkip: false, nextView: {
|
}, continueLabel: "introduction_continue", canSkip: false, nextView: {
|
||||||
IntroductionDoneView()
|
IntroductionDoneView()
|
||||||
}, nextViewSkip: {}, frontMockup: {
|
}, nextViewSkip: {}, topView: {
|
||||||
AppMockupFrontView()
|
IntroductionIconView(icon: "wifi")
|
||||||
}, backMockup: {
|
|
||||||
SymbolMockupView(symbol: "globe")
|
|
||||||
})
|
})
|
||||||
.onChange(of: scenePhase) { newValue in
|
.onChange(of: scenePhase) { newValue in
|
||||||
|
|
||||||
// alert was removed from screen, app is foreground again -> continue with intro
|
// alert was removed from screen, app is foreground again -> continue with intro
|
||||||
if newValue == .active && requestedLocalNetworkAccess {
|
if newValue == .active && requestedLocalNetworkAccess {
|
||||||
IntroductionViewController.sharedInstance.canProceed = true
|
IntroductionViewController.sharedInstance.canProceed = true
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ struct PhotoManager {
|
|||||||
|
|
||||||
/// Returns if there are photos or videos among the given URLs.
|
/// Returns if there are photos or videos among the given URLs.
|
||||||
static func hasPhotosOrVideos(at urls: [URL]) -> Bool {
|
static func hasPhotosOrVideos(at urls: [URL]) -> Bool {
|
||||||
urls.contains(where: { isImageFile(at: $0) || isVideoFile(at: $0) })
|
urls.contains(where: { EXIFUtils.isImageFile(at: $0) || EXIFUtils.isVideoFile(at: $0) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -30,9 +30,6 @@ struct PhotoManager {
|
|||||||
/// - Throws: `SaveToPhotosError` or underlying URLSession/Photos errors.
|
/// - Throws: `SaveToPhotosError` or underlying URLSession/Photos errors.
|
||||||
static func saveMediaToPhotoLibrary(from urls: [URL], openPhotosOnSuccess: Bool = true) async throws {
|
static func saveMediaToPhotoLibrary(from urls: [URL], openPhotosOnSuccess: Bool = true) async throws {
|
||||||
|
|
||||||
let candidates = urls.filter { isImageFile(at: $0) || isVideoFile(at: $0) }
|
|
||||||
guard !candidates.isEmpty else { throw SaveToPhotosError.noSavableContent }
|
|
||||||
|
|
||||||
let authStatus = await requestAddOnlyPhotoAuthorization()
|
let authStatus = await requestAddOnlyPhotoAuthorization()
|
||||||
guard authStatus == .authorized || authStatus == .limited else {
|
guard authStatus == .authorized || authStatus == .limited else {
|
||||||
throw SaveToPhotosError.permissionDenied
|
throw SaveToPhotosError.permissionDenied
|
||||||
@@ -41,16 +38,15 @@ struct PhotoManager {
|
|||||||
var imageItems: [ImageItem] = []
|
var imageItems: [ImageItem] = []
|
||||||
var videoItems: [VideoItem] = []
|
var videoItems: [VideoItem] = []
|
||||||
|
|
||||||
|
|
||||||
// Extract creation date for image and video
|
// Extract creation date for image and video
|
||||||
for sourceURL in candidates {
|
for sourceURL in urls {
|
||||||
|
let (type, date) = EXIFUtils.originalDate(from: sourceURL)
|
||||||
|
|
||||||
if isImageFile(at: sourceURL) {
|
if type == .image {
|
||||||
let exifDate = EXIFUtils.exifOriginalDate(from: sourceURL)
|
imageItems.append(ImageItem(fileURL: sourceURL, creationDate: date))
|
||||||
imageItems.append(ImageItem(fileURL: sourceURL, creationDate: exifDate))
|
}
|
||||||
} else {
|
if type == .video {
|
||||||
let videoDate = getVideoCreationDate(from: sourceURL)
|
videoItems.append(VideoItem(fileURL: sourceURL, creationDate: date))
|
||||||
videoItems.append(VideoItem(fileURL: sourceURL, creationDate: videoDate))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,40 +112,6 @@ struct PhotoManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static func isImageFile(at url: URL) -> Bool {
|
|
||||||
let ext = url.pathExtension.lowercased()
|
|
||||||
guard let type = UTType(filenameExtension: ext) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return type.conforms(to: .image)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static func isVideoFile(at url: URL) -> Bool {
|
|
||||||
let ext = url.pathExtension.lowercased()
|
|
||||||
guard let type = UTType(filenameExtension: ext) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// `.movie` is the canonical file-based video type; `.audiovisualContent`
|
|
||||||
// catches some broader cases if needed.
|
|
||||||
return type.conforms(to: .movie) || type.conforms(to: .audiovisualContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static func getVideoCreationDate(from url: URL) -> Date? {
|
|
||||||
let asset = AVURLAsset(url: url)
|
|
||||||
// AVMetadataItem that typically contains an ISO‑8601 string
|
|
||||||
guard let item = asset.creationDate,
|
|
||||||
let dateString = item.stringValue else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let formatter = ISO8601DateFormatter()
|
|
||||||
return formatter.date(from: dateString)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Requests add-only Photos permission
|
/// Requests add-only Photos permission
|
||||||
private static func requestAddOnlyPhotoAuthorization() async -> PHAuthorizationStatus {
|
private static func requestAddOnlyPhotoAuthorization() async -> PHAuthorizationStatus {
|
||||||
await withCheckedContinuation { continuation in
|
await withCheckedContinuation { continuation in
|
||||||
@@ -158,8 +120,12 @@ struct PhotoManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Private types
|
||||||
|
|
||||||
|
private extension PhotoManager {
|
||||||
private enum SaveToPhotosError: Error, LocalizedError {
|
private enum SaveToPhotosError: Error, LocalizedError {
|
||||||
case noSavableContent
|
case noSavableContent
|
||||||
case permissionDenied
|
case permissionDenied
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
//
|
|
||||||
// PieProgressView.swift
|
|
||||||
// QuickDrop
|
|
||||||
//
|
|
||||||
// Created by Leon Böttger on 16.08.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct PieProgressView: View {
|
|
||||||
let progress: CGFloat // value between 0.0 and 1.0
|
|
||||||
var size: CGFloat = 25.0
|
|
||||||
|
|
||||||
var color: Color? = nil
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
|
|
||||||
let color = color ?? .gray
|
|
||||||
|
|
||||||
ZStack {
|
|
||||||
|
|
||||||
Circle()
|
|
||||||
.stroke(lineWidth: size * 0.08)
|
|
||||||
.foregroundColor(color)
|
|
||||||
|
|
||||||
ZStack {
|
|
||||||
PieSlice(progress: progress)
|
|
||||||
.fill(color)
|
|
||||||
.rotationEffect(.degrees(-90)) // Start from the top
|
|
||||||
}
|
|
||||||
.overlay (
|
|
||||||
Image(systemName: "checkmark")
|
|
||||||
.font(.system(size: 12, weight: .bold))
|
|
||||||
.foregroundColor(color == .white ? .blue : .white)
|
|
||||||
.opacity(progress >= 1.0 ? 1.0 : 0.0) // Show checkmark only when complete
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.frame(width: size, height: size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct PieSlice: Shape {
|
|
||||||
var progress: CGFloat // from 0.0 to 1.0
|
|
||||||
|
|
||||||
func path(in rect: CGRect) -> Path {
|
|
||||||
var path = Path()
|
|
||||||
|
|
||||||
let center = CGPoint(x: rect.midX, y: rect.midY)
|
|
||||||
let radius = rect.width / 2
|
|
||||||
let endAngle = Angle(degrees: 360 * Double(progress))
|
|
||||||
|
|
||||||
path.move(to: center)
|
|
||||||
path.addArc(center: center,
|
|
||||||
radius: radius,
|
|
||||||
startAngle: .degrees(0),
|
|
||||||
endAngle: endAngle,
|
|
||||||
clockwise: false)
|
|
||||||
path.closeSubpath()
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
PieProgressView(progress: 0.76)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,8 @@ import SwiftUI
|
|||||||
import LUI
|
import LUI
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
#elseif canImport(UIKit)
|
||||||
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @escaping (AttachmentDetails) -> Void) {
|
func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @escaping (AttachmentDetails) -> Void) {
|
||||||
@@ -42,7 +44,31 @@ func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @e
|
|||||||
ignoredAttachments += 1
|
ignoredAttachments += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
checkIfAttachmentsLoaded(result: result, attachmentCount: attachments.count, ignoredAttachments: ignoredAttachments)
|
checkIfAttachmentsLoaded(result: result,
|
||||||
|
attachmentCount: attachments.count,
|
||||||
|
ignoredAttachments: ignoredAttachments,
|
||||||
|
item: item,
|
||||||
|
loadedItems: loadedItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||||||
|
let imageTypeIdentifier = attachment.registeredTypeIdentifiers.first(where: {
|
||||||
|
UTType($0)?.conforms(to: .image) == true
|
||||||
|
}) ?? UTType.image.identifier
|
||||||
|
attachment.loadItem(forTypeIdentifier: imageTypeIdentifier, options: nil) { data, _ in
|
||||||
|
if let url = imageUrl(from: data, typeIdentifier: imageTypeIdentifier) {
|
||||||
|
result.urls += [url]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log("Attachment is not an image URL or data, ignoring. All attachments: \(attachments.description)")
|
||||||
|
ignoredAttachments += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIfAttachmentsLoaded(result: result,
|
||||||
|
attachmentCount: attachments.count,
|
||||||
|
ignoredAttachments: ignoredAttachments,
|
||||||
|
item: item,
|
||||||
|
loadedItems: loadedItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -52,20 +78,28 @@ func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @e
|
|||||||
log("Found NSURL: \(url)")
|
log("Found NSURL: \(url)")
|
||||||
result.urls += [(url as URL)]
|
result.urls += [(url as URL)]
|
||||||
|
|
||||||
checkIfAttachmentsLoaded(result: result, attachmentCount: attachments.count, ignoredAttachments: ignoredAttachments)
|
checkIfAttachmentsLoaded(result: result,
|
||||||
|
attachmentCount: attachments.count,
|
||||||
|
ignoredAttachments: ignoredAttachments,
|
||||||
|
item: item,
|
||||||
|
loadedItems: loadedItems)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log("Attachment is not a URL, ignoring. All attachments: \(attachments.description)")
|
log("Attachment is not a URL, ignoring. All attachments: \(attachments.description)")
|
||||||
ignoredAttachments += 1
|
ignoredAttachments += 1
|
||||||
|
|
||||||
checkIfAttachmentsLoaded(result: result, attachmentCount: attachments.count, ignoredAttachments: ignoredAttachments)
|
checkIfAttachmentsLoaded(result: result,
|
||||||
|
attachmentCount: attachments.count,
|
||||||
|
ignoredAttachments: ignoredAttachments,
|
||||||
|
item: item,
|
||||||
|
loadedItems: loadedItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
loadTextIfPossible()
|
deliverTextIfPossible(from: item, loadedItems: loadedItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -74,83 +108,136 @@ func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @e
|
|||||||
let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
|
let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
|
||||||
extensionContext.cancelRequest(withError: cancelError)
|
extensionContext.cancelRequest(withError: cancelError)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func deliverTextIfPossible(from item: NSExtensionItem, loadedItems: @escaping (AttachmentDetails) -> Void) {
|
||||||
|
guard let details = textAttachmentDetails(from: item) else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
loadedItems(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func textAttachmentDetails(from item: NSExtensionItem) -> AttachmentDetails? {
|
||||||
|
guard let text = item.attributedContentText?.string else { return nil }
|
||||||
|
|
||||||
|
log("Found text content: \(text)")
|
||||||
|
|
||||||
func loadTextIfPossible() {
|
var result = AttachmentDetails(urls: [], textToSend: text, shortDescription: "", previewImage: nil)
|
||||||
if let text = item.attributedContentText?.string {
|
|
||||||
|
#if os(macOS)
|
||||||
log("Found text content: \(text)")
|
result.previewImage = NSImage(named: NSImage.multipleDocumentsName)
|
||||||
|
#else
|
||||||
result.textToSend = text
|
result.previewImage = Image(systemName: "text.document")
|
||||||
|
#endif
|
||||||
#if os(macOS)
|
|
||||||
result.previewImage = NSImage(named: NSImage.multipleDocumentsName)
|
let maxLength = 50
|
||||||
#else
|
let cleanedText = text.replacingOccurrences(of: "\n", with: " ").replacingOccurrences(of: "\r", with: "")
|
||||||
result.previewImage = Image(systemName: "text.document")
|
|
||||||
#endif
|
if cleanedText.count > maxLength {
|
||||||
|
let index = cleanedText.index(cleanedText.startIndex, offsetBy: maxLength)
|
||||||
let maxLength = 50
|
result.shortDescription = String(cleanedText[..<index]) + "..."
|
||||||
let cleanedText = text.replacingOccurrences(of: "\n", with: " ").replacingOccurrences(of: "\r", with: "")
|
} else {
|
||||||
|
result.shortDescription = cleanedText
|
||||||
if cleanedText.count > maxLength {
|
|
||||||
let index = cleanedText.index(cleanedText.startIndex, offsetBy: maxLength)
|
|
||||||
result.shortDescription = String(cleanedText[..<index]) + "..."
|
|
||||||
} else {
|
|
||||||
result.shortDescription = cleanedText
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
loadedItems(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func checkIfAttachmentsLoaded(result: AttachmentDetails,
|
||||||
|
attachmentCount: Int,
|
||||||
|
ignoredAttachments: Int = 0,
|
||||||
|
item: NSExtensionItem,
|
||||||
|
loadedItems: @escaping (AttachmentDetails) -> Void) {
|
||||||
|
|
||||||
func checkIfAttachmentsLoaded(result: AttachmentDetails, attachmentCount: Int, ignoredAttachments: Int = 0) {
|
var result = result
|
||||||
|
|
||||||
|
if result.urls.count == attachmentCount-ignoredAttachments {
|
||||||
|
|
||||||
var result = result
|
// Unable to load anything useful, check if there is text as last resort
|
||||||
|
if result.urls.count == 0 {
|
||||||
if result.urls.count == attachmentCount-ignoredAttachments {
|
deliverTextIfPossible(from: item, loadedItems: loadedItems)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
else if result.urls.count == 1 {
|
||||||
|
|
||||||
// Unable to load anything useful, check if there is text as last resort
|
if result.urls[0].isFileURL {
|
||||||
if result.urls.count == 0 {
|
|
||||||
loadTextIfPossible()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
else if result.urls.count == 1 {
|
|
||||||
|
|
||||||
if result.urls[0].isFileURL {
|
log("Single file URL found: \(result.urls[0])")
|
||||||
|
|
||||||
log("Single file URL found: \(result.urls[0])")
|
result.shortDescription = result.urls[0].lastPathComponent
|
||||||
|
|
||||||
result.shortDescription = result.urls[0].lastPathComponent
|
|
||||||
#if os(macOS)
|
|
||||||
result.previewImage = NSWorkspace.shared.icon(forFile: result.urls[0].path)
|
|
||||||
#else
|
|
||||||
result.previewImage = Image(systemName: "document")
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
else if result.urls[0].scheme == "http" || result.urls[0].scheme == "https" {
|
|
||||||
result.shortDescription = result.urls[0].absoluteString
|
|
||||||
#if os(macOS)
|
|
||||||
result.previewImage = NSImage(named: NSImage.networkName)
|
|
||||||
#else
|
|
||||||
result.previewImage = Image(systemName: "network")
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
result.shortDescription = String.localizedStringWithFormat("NFiles".localized(), result.urls.count)
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
result.previewImage = NSImage(named: NSImage.multipleDocumentsName)
|
result.previewImage = NSWorkspace.shared.icon(forFile: result.urls[0].path)
|
||||||
#else
|
#else
|
||||||
result.previewImage = Image(systemName: "document")
|
result.previewImage = Image(systemName: "document")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
else if result.urls[0].scheme == "http" || result.urls[0].scheme == "https" {
|
||||||
DispatchQueue.main.async {
|
result.shortDescription = result.urls[0].absoluteString
|
||||||
loadedItems(result)
|
#if os(macOS)
|
||||||
|
result.previewImage = NSImage(named: NSImage.networkName)
|
||||||
|
#else
|
||||||
|
result.previewImage = Image(systemName: "network")
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
result.shortDescription = String.localizedStringWithFormat("NFiles".localized(), result.urls.count)
|
||||||
|
#if os(macOS)
|
||||||
|
result.previewImage = NSImage(named: NSImage.multipleDocumentsName)
|
||||||
|
#else
|
||||||
|
result.previewImage = Image(systemName: "document")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
loadedItems(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func imageUrl(from data: Any?, typeIdentifier: String) -> URL? {
|
||||||
|
if let url = data as? NSURL {
|
||||||
|
log("Found image URL: \(url)")
|
||||||
|
return url as URL
|
||||||
|
}
|
||||||
|
|
||||||
|
if let data = data as? Data {
|
||||||
|
let fileExtension = UTType(typeIdentifier)?.preferredFilenameExtension ?? "png"
|
||||||
|
return writeDataToTemporaryFile(data: data, fileExtension: fileExtension)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(UIKit)
|
||||||
|
if let image = data as? UIImage,
|
||||||
|
let imageData = image.pngData() {
|
||||||
|
return writeDataToTemporaryFile(data: imageData, fileExtension: "png")
|
||||||
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
if let image = data as? NSImage,
|
||||||
|
let tiff = image.tiffRepresentation,
|
||||||
|
let rep = NSBitmapImageRep(data: tiff),
|
||||||
|
let pngData = rep.representation(using: .png, properties: [:]) {
|
||||||
|
return writeDataToTemporaryFile(data: pngData, fileExtension: "png")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func writeDataToTemporaryFile(data: Data, fileExtension: String) -> URL? {
|
||||||
|
let sanitizedExtension = fileExtension.isEmpty ? "dat" : fileExtension
|
||||||
|
let tempUrl = FileManager.default.temporaryDirectory
|
||||||
|
.appendingPathComponent("\(UUID().uuidString).\(sanitizedExtension)")
|
||||||
|
do {
|
||||||
|
try data.write(to: tempUrl)
|
||||||
|
return tempUrl
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
log("Failed to write attachment data to temp file: \(error)")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,5 +6,9 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>group.com.leonboettger.neardrop</string>
|
<string>group.com.leonboettger.neardrop</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -5,10 +5,11 @@
|
|||||||
// Created by Grishka on 12.09.2023.
|
// Created by Grishka on 12.09.2023.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AppKit
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
|
||||||
import LUI
|
import LUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
class ShareViewController: NSViewController, OutboundAppDelegate {
|
class ShareViewController: NSViewController, OutboundAppDelegate {
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
|
|
||||||
loadAttachments(with: extensionContext, loadedItems: { result in
|
loadAttachments(with: extensionContext, loadedItems: { result in
|
||||||
|
|
||||||
log("Loaded attachments: \(result)")
|
log("[ShareViewController] Loaded attachments: \(result)")
|
||||||
|
|
||||||
self.urls = result.urls
|
self.urls = result.urls
|
||||||
self.textToSend = result.textToSend
|
self.textToSend = result.textToSend
|
||||||
@@ -94,6 +95,9 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
|
|
||||||
progressDeviceIconWrap!.wantsLayer = true
|
progressDeviceIconWrap!.wantsLayer = true
|
||||||
progressDeviceIconWrap!.layer!.masksToBounds = false
|
progressDeviceIconWrap!.layer!.masksToBounds = false
|
||||||
|
progressDeviceSecondaryIcon?.image = NSImage(named: "MenuBarIcon")
|
||||||
|
progressDeviceSecondaryIcon?.image?.isTemplate = true
|
||||||
|
progressDeviceSecondaryIcon?.contentTintColor = .white
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -107,13 +111,11 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
|
|
||||||
|
|
||||||
override func viewWillDisappear() {
|
override func viewWillDisappear() {
|
||||||
log("ShareViewController: viewWillDisappear")
|
log("[ShareViewController] ShareViewController: viewWillDisappear")
|
||||||
|
|
||||||
timeoutDispatchWorkItem?.cancel()
|
timeoutDispatchWorkItem?.cancel()
|
||||||
|
|
||||||
if chosenDevice == nil {
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
|
||||||
}
|
|
||||||
NearbyConnectionManager.shared.removeOutboundAppDelegate(self)
|
NearbyConnectionManager.shared.removeOutboundAppDelegate(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,32 +253,50 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
progressView?.animator().isHidden = false
|
progressView?.animator().isHidden = false
|
||||||
progressDeviceName?.stringValue = getDeviceName(device: device)
|
progressDeviceName?.stringValue = getDeviceName(device: device)
|
||||||
progressDeviceIcon?.image = imageForDeviceType(type: device.type)
|
progressDeviceIcon?.image = imageForDeviceType(type: device.type)
|
||||||
|
progressDeviceSecondaryIcon?.isHidden = !device.isQuickDropPeer
|
||||||
progressProgressBar?.startAnimation(nil)
|
progressProgressBar?.startAnimation(nil)
|
||||||
progressState?.stringValue = "Preparing".localized()
|
progressState?.stringValue = "Preparing".localized()
|
||||||
chosenDevice = device
|
chosenDevice = device
|
||||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
|
||||||
|
|
||||||
runAfter(seconds: 0.3) {
|
runAfter(seconds: 0.3) {
|
||||||
NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: self.urls, textToSend: self.textToSend)
|
NearbyConnectionManager.shared.startOutgoingTransfer(
|
||||||
|
deviceID: device.id!,
|
||||||
self.progressState?.stringValue = "Connecting".localized()
|
delegate: self,
|
||||||
|
urls: self.urls,
|
||||||
let timeoutAlert = DispatchWorkItem {
|
textToSend: self.textToSend,
|
||||||
if !self.connectionEstablished {
|
preparationFinished: {
|
||||||
AudioManager.playErrorSound()
|
self.progressState?.stringValue = "Connecting".localized()
|
||||||
let alert = NSAlert()
|
|
||||||
alert.alertStyle = .critical
|
|
||||||
|
|
||||||
alert.messageText = "TimeoutTitle".localized()
|
let timeoutAlert = DispatchWorkItem {
|
||||||
alert.informativeText = "TimeoutDescription".localized()
|
if !self.connectionEstablished {
|
||||||
alert.addButton(withTitle: "TimeoutButton".localized())
|
AudioManager.playErrorSound()
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.alertStyle = .critical
|
||||||
|
|
||||||
|
alert.messageText = "TimeoutTitle".localized()
|
||||||
|
|
||||||
|
if #available(macOS 15.0, *) {
|
||||||
|
alert.informativeText = "TimeoutDescription".localized()
|
||||||
|
alert.addButton(withTitle: "TimeoutButton".localized())
|
||||||
|
alert.addButton(withTitle: "CloseAlert".localized())
|
||||||
|
} else {
|
||||||
|
alert.informativeText = "TimeoutDescriptionLegacy".localized()
|
||||||
|
alert.addButton(withTitle: "TimeoutButtonLegacy".localized())
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.beginSheetModal(for: self.view.window!) { response in
|
||||||
|
if #available(macOS 15.0, *), response == .alertFirstButtonReturn {
|
||||||
|
openPrivacyAndSecuritySettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
alert.beginSheetModal(for: self.view.window!) { _ in }
|
self.timeoutDispatchWorkItem = timeoutAlert
|
||||||
}
|
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0, execute: timeoutAlert)
|
||||||
}
|
},
|
||||||
|
receiverAuthenticationPolicy: .none
|
||||||
self.timeoutDispatchWorkItem = timeoutAlert
|
)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0, execute: timeoutAlert)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,18 +321,18 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
let isRunning = runningApps.contains { $0.bundleIdentifier == bundleIdentifier }
|
let isRunning = runningApps.contains { $0.bundleIdentifier == bundleIdentifier }
|
||||||
|
|
||||||
if !isRunning {
|
if !isRunning {
|
||||||
log("Launching main app")
|
log("[ShareViewController] Launching main app")
|
||||||
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) {
|
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) {
|
||||||
let configuration = NSWorkspace.OpenConfiguration()
|
let configuration = NSWorkspace.OpenConfiguration()
|
||||||
NSWorkspace.shared.openApplication(at: url, configuration: configuration) { app, error in
|
NSWorkspace.shared.openApplication(at: url, configuration: configuration) { app, error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
log("Failed to launch application: \(error)")
|
log("[ShareViewController] Failed to launch application: \(error)")
|
||||||
} else {
|
} else {
|
||||||
log("Application launched successfully")
|
log("[ShareViewController] Application launched successfully")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log("Could not find application with bundle identifier \(bundleIdentifier)")
|
log("[ShareViewController] Could not find application with bundle identifier \(bundleIdentifier)")
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||||
@@ -324,6 +344,9 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
NearbyConnectionManager.shared.startDeviceDiscovery()
|
NearbyConnectionManager.shared.startDeviceDiscovery()
|
||||||
scheduleAutomaticQrCodeView()
|
scheduleAutomaticQrCodeView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force local network access prompt if not already granted
|
||||||
|
let _ = DeviceToDeviceHeuristicScanner.shared.hasLocalNetworkAccess(completion: {_ in })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -371,8 +394,11 @@ extension ShareViewController: NSCollectionViewDataSource {
|
|||||||
|
|
||||||
let device = foundDevices[indexPath[1]]
|
let device = foundDevices[indexPath[1]]
|
||||||
|
|
||||||
collectionViewItem.textField?.stringValue = getDeviceName(device: device)
|
collectionViewItem.configure(
|
||||||
collectionViewItem.imageView?.image = imageForDeviceType(type: device.type)
|
deviceName: getDeviceName(device: device),
|
||||||
|
deviceImage: imageForDeviceType(type: device.type),
|
||||||
|
showsQuickDropPeerMarker: device.isQuickDropPeer && device.type == .phone
|
||||||
|
)
|
||||||
collectionViewItem.clickHandler = {
|
collectionViewItem.clickHandler = {
|
||||||
self.selectDevice(device: device)
|
self.selectDevice(device: device)
|
||||||
}
|
}
|
||||||
@@ -386,6 +412,12 @@ extension ShareViewController: NSCollectionViewDataSource {
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
return "UnknownDevice".localized()
|
return "AndroidDevice".localized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fileprivate func openPrivacyAndSecuritySettings() {
|
||||||
|
guard let url = URL(string: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension") else { return }
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
@@ -177,17 +177,17 @@ Gw
|
|||||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="rb3-78-PXe"/>
|
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="rb3-78-PXe"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
<imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="BEO-kU-a2Q">
|
<imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="BEO-kU-a2Q">
|
||||||
<rect key="frame" x="24" y="-8" width="32" height="32"/>
|
<rect key="frame" x="16.800000000000001" y="16.800000000000001" width="14.4" height="14.4"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="32" id="05I-Ef-84Z"/>
|
<constraint firstAttribute="height" constant="14.4" id="05I-Ef-84Z"/>
|
||||||
<constraint firstAttribute="width" constant="32" id="V0g-Fz-HWH"/>
|
<constraint firstAttribute="width" constant="14.4" id="V0g-Fz-HWH"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSCaution" id="k1c-Xl-fTQ"/>
|
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="k1c-Xl-fTQ"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="trailing" secondItem="BEO-kU-a2Q" secondAttribute="trailing" constant="-8" id="59U-Zi-ESa"/>
|
<constraint firstItem="BEO-kU-a2Q" firstAttribute="centerX" secondItem="84U-lB-SRB" secondAttribute="centerX" id="59U-Zi-ESa"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="BEO-kU-a2Q" secondAttribute="bottom" constant="-8" id="8ty-pq-miR"/>
|
<constraint firstItem="BEO-kU-a2Q" firstAttribute="centerY" secondItem="84U-lB-SRB" secondAttribute="centerY" id="8ty-pq-miR"/>
|
||||||
<constraint firstItem="84U-lB-SRB" firstAttribute="top" secondItem="jBg-Ih-Gac" secondAttribute="top" id="MB9-UI-2mb"/>
|
<constraint firstItem="84U-lB-SRB" firstAttribute="top" secondItem="jBg-Ih-Gac" secondAttribute="top" id="MB9-UI-2mb"/>
|
||||||
<constraint firstItem="84U-lB-SRB" firstAttribute="leading" secondItem="jBg-Ih-Gac" secondAttribute="leading" id="b23-hJ-aWT"/>
|
<constraint firstItem="84U-lB-SRB" firstAttribute="leading" secondItem="jBg-Ih-Gac" secondAttribute="leading" id="b23-hJ-aWT"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="84U-lB-SRB" secondAttribute="trailing" id="eKl-3V-K49"/>
|
<constraint firstAttribute="trailing" secondItem="84U-lB-SRB" secondAttribute="trailing" id="eKl-3V-K49"/>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Cocoa
|
|||||||
|
|
||||||
class DeviceListCell: NSCollectionViewItem {
|
class DeviceListCell: NSCollectionViewItem {
|
||||||
public var clickHandler: (() -> Void)?
|
public var clickHandler: (() -> Void)?
|
||||||
|
@IBOutlet private var peerMarkerView: NSImageView?
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
@@ -17,10 +18,20 @@ class DeviceListCell: NSCollectionViewItem {
|
|||||||
btn.setButtonType(.momentaryPushIn)
|
btn.setButtonType(.momentaryPushIn)
|
||||||
btn.action = #selector(onClick)
|
btn.action = #selector(onClick)
|
||||||
btn.target = self
|
btn.target = self
|
||||||
|
|
||||||
|
peerMarkerView?.image = NSImage(named: "MenuBarIcon")
|
||||||
|
peerMarkerView?.image?.isTemplate = true
|
||||||
|
peerMarkerView?.contentTintColor = .white
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func onClick(_: Any?) {
|
@IBAction func onClick(_: Any?) {
|
||||||
guard let handler = clickHandler else { return }
|
guard let handler = clickHandler else { return }
|
||||||
handler()
|
handler()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configure(deviceName: String, deviceImage: NSImage, showsQuickDropPeerMarker: Bool) {
|
||||||
|
textField?.stringValue = deviceName
|
||||||
|
imageView?.image = deviceImage
|
||||||
|
peerMarkerView?.isHidden = !showsQuickDropPeerMarker
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -36,10 +36,20 @@
|
|||||||
</constraints>
|
</constraints>
|
||||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="QGU-rR-rJQ"/>
|
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="QGU-rR-rJQ"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
|
<imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="89K-DQ-f8g">
|
||||||
|
<rect key="frame" x="95.799999999999997" y="58.799999999999997" width="14.4" height="14.4"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="14.4" id="Lfh-Ha-Njl"/>
|
||||||
|
<constraint firstAttribute="height" constant="14.4" id="Wm1-YW-85X"/>
|
||||||
|
</constraints>
|
||||||
|
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="0fp-y5-fj9"/>
|
||||||
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
<constraint firstItem="89K-DQ-f8g" firstAttribute="centerX" secondItem="O1J-am-CoK" secondAttribute="centerX" id="Bja-VK-UQf"/>
|
||||||
<constraint firstItem="O1J-am-CoK" firstAttribute="centerX" secondItem="Hz6-mo-xeY" secondAttribute="centerX" id="R76-2L-pXb"/>
|
<constraint firstItem="O1J-am-CoK" firstAttribute="centerX" secondItem="Hz6-mo-xeY" secondAttribute="centerX" id="R76-2L-pXb"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="QPM-sE-maP" secondAttribute="bottom" id="Ryn-nY-yZp"/>
|
<constraint firstAttribute="bottom" secondItem="QPM-sE-maP" secondAttribute="bottom" id="Ryn-nY-yZp"/>
|
||||||
|
<constraint firstItem="89K-DQ-f8g" firstAttribute="centerY" secondItem="O1J-am-CoK" secondAttribute="centerY" id="Yeh-mY-trP"/>
|
||||||
<constraint firstItem="O1J-am-CoK" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="ZFA-S9-fpT"/>
|
<constraint firstItem="O1J-am-CoK" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="ZFA-S9-fpT"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="QPM-sE-maP" secondAttribute="trailing" id="eOl-Va-HXq"/>
|
<constraint firstAttribute="trailing" secondItem="QPM-sE-maP" secondAttribute="trailing" id="eOl-Va-HXq"/>
|
||||||
<constraint firstItem="QPM-sE-maP" firstAttribute="top" secondItem="O1J-am-CoK" secondAttribute="bottom" constant="10" id="leg-nD-mHB"/>
|
<constraint firstItem="QPM-sE-maP" firstAttribute="top" secondItem="O1J-am-CoK" secondAttribute="bottom" constant="10" id="leg-nD-mHB"/>
|
||||||
@@ -53,6 +63,7 @@
|
|||||||
<collectionViewItem id="1Lb-iJ-Nar" customClass="DeviceListCell" customModule="QuickDropShareExtension" customModuleProvider="target">
|
<collectionViewItem id="1Lb-iJ-Nar" customClass="DeviceListCell" customModule="QuickDropShareExtension" customModuleProvider="target">
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="imageView" destination="O1J-am-CoK" id="zQn-mV-bRy"/>
|
<outlet property="imageView" destination="O1J-am-CoK" id="zQn-mV-bRy"/>
|
||||||
|
<outlet property="peerMarkerView" destination="89K-DQ-f8g" id="iWY-ru-o1h"/>
|
||||||
<outlet property="textField" destination="QPM-sE-maP" id="s0j-Qq-7iA"/>
|
<outlet property="textField" destination="QPM-sE-maP" id="s0j-Qq-7iA"/>
|
||||||
<outlet property="view" destination="Hz6-mo-xeY" id="gZn-20-o77"/>
|
<outlet property="view" destination="Hz6-mo-xeY" id="gZn-20-o77"/>
|
||||||
</connections>
|
</connections>
|
||||||
|
|||||||
@@ -8,5 +8,9 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>group.com.leonboettger.neardrop</string>
|
<string>group.com.leonboettger.neardrop</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.leonboettger.neardrop</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||