Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions AirSync.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
objects = {

/* Begin PBXBuildFile section */
B94971602F62509A00F59BC9 /* BigInt in Frameworks */ = {isa = PBXBuildFile; productRef = B949715F2F62509A00F59BC9 /* BigInt */; };
B94971632F6250DE00F59BC9 /* ASN1 in Frameworks */ = {isa = PBXBuildFile; productRef = B94971622F6250DE00F59BC9 /* ASN1 */; };
B94971662F62514500F59BC9 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = B94971652F62514500F59BC9 /* SwiftECC */; };
B94971692F62525800F59BC9 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = B94971682F62525800F59BC9 /* SwiftProtobuf */; };
B949716B2F62525800F59BC9 /* SwiftProtobufPluginLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = B949716A2F62525800F59BC9 /* SwiftProtobufPluginLibrary */; };
B995A3332E4D2B3F00FA7A41 /* AppIcon-uni.icon in Resources */ = {isa = PBXBuildFile; fileRef = B995A3322E4D2B3F00FA7A41 /* AppIcon-uni.icon */; };
B9AEBC0A2E6235D3006BA027 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = B9AEBC092E6235D3006BA027 /* Sparkle */; };
B9B1C00D2E94E15D0005F6CB /* LottieUI in Frameworks */ = {isa = PBXBuildFile; productRef = B9B1C00C2E94E15D0005F6CB /* LottieUI */; };
Expand Down Expand Up @@ -52,11 +57,16 @@
buildActionMask = 2147483647;
files = (
B9D2631B2F60D97900628704 /* Sentry in Frameworks */,
B94971602F62509A00F59BC9 /* BigInt in Frameworks */,
B9D263292F60D9CF00628704 /* SentrySwiftUI in Frameworks */,
B94971692F62525800F59BC9 /* SwiftProtobuf in Frameworks */,
B9AEBC0A2E6235D3006BA027 /* Sparkle in Frameworks */,
B94971662F62514500F59BC9 /* SwiftECC in Frameworks */,
B9D742FC2E39CF850053128A /* Swifter in Frameworks */,
B9C3181E2E37AA1D00367F16 /* QRCode in Frameworks */,
B9B1C00D2E94E15D0005F6CB /* LottieUI in Frameworks */,
B949716B2F62525800F59BC9 /* SwiftProtobufPluginLibrary in Frameworks */,
B94971632F6250DE00F59BC9 /* ASN1 in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -114,6 +124,11 @@
B9B1C00C2E94E15D0005F6CB /* LottieUI */,
B9D2631A2F60D97900628704 /* Sentry */,
B9D263282F60D9CF00628704 /* SentrySwiftUI */,
B949715F2F62509A00F59BC9 /* BigInt */,
B94971622F6250DE00F59BC9 /* ASN1 */,
B94971652F62514500F59BC9 /* SwiftECC */,
B94971682F62525800F59BC9 /* SwiftProtobuf */,
B949716A2F62525800F59BC9 /* SwiftProtobufPluginLibrary */,
);
productName = "airsync-mac";
productReference = B9673B572E35A2A1006D284A /* AirSync.app */;
Expand Down Expand Up @@ -149,6 +164,10 @@
B9AEBC082E6235D3006BA027 /* XCRemoteSwiftPackageReference "Sparkle" */,
B9B1C00B2E94E15D0005F6CB /* XCRemoteSwiftPackageReference "LottieUI" */,
B9D263162F60D26700628704 /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
B949715E2F62509A00F59BC9 /* XCRemoteSwiftPackageReference "BigInt" */,
B94971612F6250DE00F59BC9 /* XCRemoteSwiftPackageReference "ASN1" */,
B94971642F62514500F59BC9 /* XCRemoteSwiftPackageReference "SwiftECC" */,
B94971672F62525800F59BC9 /* XCRemoteSwiftPackageReference "swift-protobuf" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = B9673B582E35A2A1006D284A /* Products */;
Expand Down Expand Up @@ -542,6 +561,38 @@
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
B949715E2F62509A00F59BC9 /* XCRemoteSwiftPackageReference "BigInt" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/leif-ibsen/BigInt";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.22.0;
};
};
B94971612F6250DE00F59BC9 /* XCRemoteSwiftPackageReference "ASN1" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/leif-ibsen/ASN1";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.7.0;
};
};
B94971642F62514500F59BC9 /* XCRemoteSwiftPackageReference "SwiftECC" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/leif-ibsen/SwiftECC";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.5.0;
};
};
B94971672F62525800F59BC9 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-protobuf.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.36.0;
};
};
B9AEBC082E6235D3006BA027 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sparkle-project/Sparkle";
Expand Down Expand Up @@ -585,6 +636,31 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
B949715F2F62509A00F59BC9 /* BigInt */ = {
isa = XCSwiftPackageProductDependency;
package = B949715E2F62509A00F59BC9 /* XCRemoteSwiftPackageReference "BigInt" */;
productName = BigInt;
};
B94971622F6250DE00F59BC9 /* ASN1 */ = {
isa = XCSwiftPackageProductDependency;
package = B94971612F6250DE00F59BC9 /* XCRemoteSwiftPackageReference "ASN1" */;
productName = ASN1;
};
B94971652F62514500F59BC9 /* SwiftECC */ = {
isa = XCSwiftPackageProductDependency;
package = B94971642F62514500F59BC9 /* XCRemoteSwiftPackageReference "SwiftECC" */;
productName = SwiftECC;
};
B94971682F62525800F59BC9 /* SwiftProtobuf */ = {
isa = XCSwiftPackageProductDependency;
package = B94971672F62525800F59BC9 /* XCRemoteSwiftPackageReference "swift-protobuf" */;
productName = SwiftProtobuf;
};
B949716A2F62525800F59BC9 /* SwiftProtobufPluginLibrary */ = {
isa = XCSwiftPackageProductDependency;
package = B94971672F62525800F59BC9 /* XCRemoteSwiftPackageReference "swift-protobuf" */;
productName = SwiftProtobufPluginLibrary;
};
B9AEBC092E6235D3006BA027 /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = B9AEBC082E6235D3006BA027 /* XCRemoteSwiftPackageReference "Sparkle" */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions airsync-mac/AirSync-Bridging-Header.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//
// AirSync-Bridging-Header.h
// AirSync
//

#import "Core/QuickShare/NDNotificationCenterHackery.h"
132 changes: 95 additions & 37 deletions airsync-mac/Components/Custom/DropTargetModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,68 +7,119 @@

import SwiftUI
import UniformTypeIdentifiers
import Foundation
import AppKit

struct DropTargetModifier: ViewModifier {
@State private var isTargeted = false
@State private var dragLabel: String = ""
let appState: AppState

func body(content: Content) -> some View {
content
.onDrop(of: [.plainText, .fileURL], isTargeted: $isTargeted) { providers in
handleDrop(providers: providers)
return true
}
.onDrop(of: [.plainText, .fileURL], delegate: QuickShareDropDelegate(
appState: appState,
isTargeted: $isTargeted,
dragLabel: $dragLabel
))
.overlay(
Group {
if isTargeted {
DropTargetOverlay()
DropTargetOverlay(label: dragLabel)
}
}
)
}
}

private func handleDrop(providers: [NSItemProvider]) {
guard appState.device != nil else {
// Show notification if no device connected
appState.postNativeNotification(
id: "no_device",
appName: "AirSync",
title: "No Device Connected",
body: "Connect an Android device first to send text"
)
return
struct QuickShareDropDelegate: DropDelegate {
let appState: AppState
@Binding var isTargeted: Bool
@Binding var dragLabel: String

private func updateLabel() {
let optionPressed = NSEvent.modifierFlags.contains(.option)
if optionPressed {
dragLabel = Localizer.shared.text("quickshare.drop.pick_device")
} else if let deviceName = appState.device?.name {
dragLabel = String(format: Localizer.shared.text("quickshare.drop.send_to"), deviceName)
} else {
dragLabel = Localizer.shared.text("quickshare.drop.pick_device")
}
}

func dropEntered(info: DropInfo) {
isTargeted = true
updateLabel()
}

func dropUpdated(info: DropInfo) -> DropOperation? {
updateLabel()
return .copy
}

func dropExited(info: DropInfo) {
isTargeted = false
}

func performDrop(info: DropInfo) -> Bool {
isTargeted = false

let providers = info.itemProviders(for: [.plainText, .fileURL])
handleDrop(providers: providers)
return true
}

private func handleDrop(providers: [NSItemProvider]) {
let group = DispatchGroup()
var urls: [URL] = []
let urlLock = NSLock()
var text: String?
let textLock = NSLock()

for provider in providers {
if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, error in
if let text = item as? String ?? (item as? Data).flatMap({ String(data: $0, encoding: .utf8) }) {
DispatchQueue.main.async {
sendTextToDevice(text)
}
if provider.canLoadObject(ofClass: URL.self) {
group.enter()
_ = provider.loadObject(ofClass: URL.self) { url, error in
if let url = url {
urlLock.lock()
urls.append(url)
urlLock.unlock()
}
group.leave()
}
return
} else if provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) {
provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { item, error in
guard let url = (item as? URL) ?? (item as? Data).flatMap({ URL(dataRepresentation: $0, relativeTo: nil) }) else { return }

// Initiate file transfer
DispatchQueue.main.async {
WebSocketServer.shared.sendFile(url: url)
} else if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
group.enter()
provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, error in
if let s = item as? String ?? (item as? Data).flatMap({ String(data: $0, encoding: .utf8) }) {
textLock.lock()
text = s
textLock.unlock()
}
group.leave()
}
return
}
}
}

private func sendTextToDevice(_ text: String) {
appState.sendClipboardToAndroid(text: text)
group.notify(queue: .main) {
if !urls.isEmpty {
let optionPressed = NSEvent.modifierFlags.contains(.option)
let connectedDeviceName = appState.device?.name
let targetName = (!optionPressed) ? connectedDeviceName : nil

QuickShareManager.shared.startDiscovery(autoTargetName: targetName)
QuickShareManager.shared.transferURLs = urls
appState.showingQuickShareTransfer = true
} else if let text = text {
appState.sendClipboardToAndroid(text: text)
}
}
}
}

struct DropTargetOverlay: View {
let label: String

var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 12)
Expand All @@ -78,17 +129,24 @@ struct DropTargetOverlay: View {
.stroke(Color.accentColor, style: StrokeStyle(lineWidth: 3, dash: [10, 5]))
.padding(8)

Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 64, weight: .semibold))
.foregroundColor(.accentColor)
VStack(spacing: 16) {
Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 48, weight: .semibold))
.foregroundColor(.accentColor)

Text(label)
.font(.headline)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
}
.padding(4)
.allowsHitTesting(false)
}
}

extension View {
func dropTarget(appState: AppState) -> some View {
func dropTarget(appState: AppState, autoTargetName: String? = nil) -> some View {
self.modifier(DropTargetModifier(appState: appState))
}
}
Loading