mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Slide in for tast view
This commit is contained in:
1 parent
a5781e6e96
commit
f11943475c
2 files changed
+119
-73
No files matched your search
@@ -13,6 +13,8 @@ let toastViewSize = CGSize(width: 420, height: 75)
|
||||
let actionColumnWidth: CGFloat = 120
|
||||
let actionColumnWidthEnd: CGFloat = 165
|
||||
let toastCornerRadius: CGFloat = 20
|
||||
let toastTrailingPadding: CGFloat = 20
|
||||
let toastSlideDistance: CGFloat = 420
|
||||
|
||||
struct QuickDropToastView: View {
|
||||
@ObservedObject var settings = Settings.sharedInstance
|
||||
@@ -35,7 +37,7 @@ struct QuickDropToastView: View {
|
||||
private func startAutoHide(_ actions: ReceiveModel.ToastViewAction) {
|
||||
autoHider.cancel()
|
||||
autoHider = DispatchWorkItem(block: {
|
||||
actions.closeToastAction()
|
||||
actions.autoHideAction()
|
||||
})
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + actions.autoHideDelay, execute: autoHider)
|
||||
@@ -250,6 +252,33 @@ struct QuickDropToastView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct QuickDropToastHostView: View {
|
||||
@ObservedObject var receiveModel: ReceiveModel
|
||||
let onCancel: () -> Void
|
||||
|
||||
private let slideAnimation = Animation.smooth
|
||||
private let fadeAnimation = Animation.easeInOut
|
||||
|
||||
var body: some View {
|
||||
let isVisible = 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, 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: receiveModel.toastIsVisible)
|
||||
.animation(animation, value: receiveModel.toastDismissStyle)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
|
||||
.padding(.trailing, toastTrailingPadding)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate struct QuickDropToastViewButton: View {
|
||||
let title: String
|
||||
@@ -455,7 +484,7 @@ struct QuickDropToastView_Previews: PreviewProvider {
|
||||
.background(previewWallpaper)
|
||||
.onAppear {
|
||||
model.progress = 1
|
||||
model.toastActions = .init(completionMessageKey: "Saved", autoHideDelay: 10, openFilesAction: {}, importPhotosAction: {}, closeToastAction: {})
|
||||
model.toastActions = .init(completionMessageKey: "Saved", autoHideDelay: 10, openFilesAction: {}, importPhotosAction: {}, closeToastAction: {}, autoHideAction: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,11 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
@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 toastWindow: NSWindow?
|
||||
private var toastHosting: NSHostingView<QuickDropToastView>?
|
||||
private var toastHosting: NSHostingView<QuickDropToastHostView>?
|
||||
private let monitor = AllowedWorkMonitor()
|
||||
#endif
|
||||
|
||||
@@ -282,13 +284,17 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
} : nil
|
||||
|
||||
let closeToastAction = {
|
||||
self.hideQuickDropToast()
|
||||
self.hideQuickDropToast(style: .fade)
|
||||
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
||||
}
|
||||
let autoHideAction = {
|
||||
self.hideQuickDropToast(style: .slide)
|
||||
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
withAnimation {
|
||||
self.toastActions = ToastViewAction(completionMessageKey: completionKey, autoHideDelay: autoHideDelay, 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
|
||||
@@ -369,12 +375,16 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
} : nil
|
||||
|
||||
let closeToastAction = {
|
||||
self.hideQuickDropToast()
|
||||
self.hideQuickDropToast(style: .fade)
|
||||
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
||||
}
|
||||
let autoHideAction = {
|
||||
self.hideQuickDropToast(style: .slide)
|
||||
showReviewIfAppropriate(currentTransmissionCount: currentCount)
|
||||
}
|
||||
|
||||
withAnimation {
|
||||
self.toastActions = ToastViewAction(completionMessageKey: completionKey, autoHideDelay: autoHideDelay, 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
|
||||
@@ -419,69 +429,71 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
func showQuickDropToast(for connectionID: String) {
|
||||
guard toastWindow == nil else { return }
|
||||
|
||||
let contentView = QuickDropToastView(
|
||||
receiveModel: self,
|
||||
onCancel: {
|
||||
NearbyConnectionManager.shared.cancelTransfer(id: connectionID)
|
||||
DispatchQueue.main.async {
|
||||
guard self.toastWindow == nil else { return }
|
||||
|
||||
self.toastIsVisible = false
|
||||
self.toastDismissStyle = .slide
|
||||
|
||||
let contentView = QuickDropToastHostView(
|
||||
receiveModel: self,
|
||||
onCancel: {
|
||||
NearbyConnectionManager.shared.cancelTransfer(id: connectionID)
|
||||
}
|
||||
)
|
||||
|
||||
let hostingView = NSHostingView(rootView: contentView)
|
||||
|
||||
// Prefer the screen under the mouse, then the key window’s screen, then main
|
||||
let screen = NSScreen.screens.first { $0.frame.contains(NSEvent.mouseLocation) }
|
||||
?? NSApp.keyWindow?.screen
|
||||
?? NSScreen.main
|
||||
|
||||
guard let screen else { return }
|
||||
|
||||
let frame = screen.frame
|
||||
let visible = screen.visibleFrame
|
||||
|
||||
let marginX: CGFloat = 20
|
||||
let marginFromTop: CGFloat = 20
|
||||
|
||||
// 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 extraTopInset = menuBarHidden ? NSStatusBar.system.thickness : 0
|
||||
|
||||
let targetX = visible.maxX - toastViewSize.width - marginX
|
||||
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)
|
||||
|
||||
hostingView.frame.size = targetFrame.size
|
||||
|
||||
let window = NSWindow(contentRect: targetFrame,
|
||||
styleMask: [.borderless],
|
||||
backing: .buffered,
|
||||
defer: false)
|
||||
window.isOpaque = false
|
||||
window.backgroundColor = .clear
|
||||
window.hasShadow = true
|
||||
window.level = .statusBar
|
||||
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||
window.contentView = hostingView
|
||||
window.ignoresMouseEvents = false
|
||||
window.alphaValue = 1
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
|
||||
self.toastWindow = window
|
||||
self.toastHosting = hostingView
|
||||
self.toastIsVisible = false
|
||||
DispatchQueue.main.async {
|
||||
self.toastIsVisible = true
|
||||
}
|
||||
)
|
||||
|
||||
let hostingView = NSHostingView(rootView: contentView)
|
||||
hostingView.frame.size = toastViewSize
|
||||
|
||||
// Prefer the screen under the mouse, then the key window’s screen, then main
|
||||
let screen = NSScreen.screens.first { $0.frame.contains(NSEvent.mouseLocation) }
|
||||
?? NSApp.keyWindow?.screen
|
||||
?? NSScreen.main
|
||||
|
||||
guard let screen else { return }
|
||||
|
||||
let frame = screen.frame
|
||||
let visible = screen.visibleFrame
|
||||
|
||||
let marginX: CGFloat = 20
|
||||
let marginFromTop: CGFloat = 20
|
||||
|
||||
// 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 extraTopInset = menuBarHidden ? NSStatusBar.system.thickness : 0
|
||||
|
||||
let targetX = visible.maxX - toastViewSize.width - marginX
|
||||
let targetY = visible.maxY - toastViewSize.height - marginFromTop - extraTopInset
|
||||
|
||||
// Start slightly above the final spot for a smooth slide-in
|
||||
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],
|
||||
backing: .buffered,
|
||||
defer: false)
|
||||
window.isOpaque = false
|
||||
window.backgroundColor = .clear
|
||||
window.hasShadow = true
|
||||
window.level = .statusBar
|
||||
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||
window.contentView = hostingView
|
||||
window.ignoresMouseEvents = false
|
||||
window.alphaValue = 0
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
|
||||
self.toastWindow = window
|
||||
self.toastHosting = hostingView
|
||||
|
||||
NSAnimationContext.runAnimationGroup { ctx in
|
||||
ctx.duration = 0.35
|
||||
ctx.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
window.animator().setFrameOrigin(CGPoint(x: targetX, y: targetY))
|
||||
window.animator().alphaValue = 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func hideQuickDropToast() {
|
||||
guard let window = toastWindow,
|
||||
let screen = NSScreen.main else {
|
||||
func hideQuickDropToast(style: ToastDismissStyle = .fade) {
|
||||
guard let window = toastWindow else {
|
||||
self.toastWindow = nil
|
||||
self.toastHosting = nil
|
||||
self.toastActions = nil
|
||||
@@ -489,21 +501,19 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
let offscreenY = screen.visibleFrame.origin.y + screen.visibleFrame.height + 20
|
||||
self.toastDismissStyle = style
|
||||
self.toastIsVisible = false
|
||||
|
||||
NSAnimationContext.runAnimationGroup({ ctx in
|
||||
ctx.duration = 0.35
|
||||
ctx.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
window.animator().setFrameOrigin(CGPoint(x: window.frame.origin.x, y: offscreenY))
|
||||
window.animator().alphaValue = 0
|
||||
}, completionHandler: {
|
||||
let cleanupDelay: TimeInterval = (style == .slide) ? 0.5 : 0.3
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + cleanupDelay) {
|
||||
window.orderOut(nil)
|
||||
self.toastWindow = nil
|
||||
self.toastHosting = nil
|
||||
self.toastActions = nil
|
||||
self.consentState = nil
|
||||
self.activeDeviceName = nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -513,6 +523,13 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
let openFilesAction: (() -> ())?
|
||||
let importPhotosAction: (() -> ())?
|
||||
let closeToastAction: () -> ()
|
||||
let autoHideAction: () -> ()
|
||||
}
|
||||
|
||||
|
||||
enum ToastDismissStyle {
|
||||
case slide
|
||||
case fade
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user