mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 23:54:32 +00:00
Restored files from old Mac, force pushing
This commit is contained in:
parent
5dd95228b8
commit
5040381840
126 changed files with 10431 additions and 67 deletions
107
DynamicIsland/components/Notch/BoringExtrasMenu.swift
Normal file
107
DynamicIsland/components/Notch/BoringExtrasMenu.swift
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
//
|
||||
// BoringExtrasMenu.swift
|
||||
// boringNotch
|
||||
//
|
||||
// Created by Harsh Vardhan Goswami on 04/08/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BoringLargeButtons: View {
|
||||
var action: () -> Void
|
||||
var icon: Image
|
||||
var title: String
|
||||
var body: some View {
|
||||
Button (
|
||||
action:action,
|
||||
label: {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12.0).fill(.black).frame(width: 70, height: 70)
|
||||
VStack(spacing: 8) {
|
||||
icon.resizable()
|
||||
.aspectRatio(contentMode: .fit).frame(width:20)
|
||||
Text(title).font(.body)
|
||||
}
|
||||
}
|
||||
}).buttonStyle(PlainButtonStyle()).shadow(color: .black.opacity(0.5), radius: 10)
|
||||
}
|
||||
}
|
||||
|
||||
struct BoringExtrasMenu : View {
|
||||
@ObservedObject var vm: DynamicIslandViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack{
|
||||
HStack(spacing: 20) {
|
||||
hide
|
||||
settings
|
||||
close
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var github: some View {
|
||||
BoringLargeButtons(
|
||||
action: {
|
||||
NSWorkspace.shared.open(productPage)
|
||||
},
|
||||
icon: Image(.github),
|
||||
title: "Checkout"
|
||||
)
|
||||
}
|
||||
|
||||
var donate: some View {
|
||||
BoringLargeButtons(
|
||||
action: {
|
||||
NSWorkspace.shared.open(sponsorPage)
|
||||
},
|
||||
icon: Image(systemName: "heart.fill"),
|
||||
title: "Love Us"
|
||||
)
|
||||
}
|
||||
|
||||
var settings: some View {
|
||||
SettingsLink(label: {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12.0).fill(.black).frame(width: 70, height: 70)
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "gear").resizable()
|
||||
.aspectRatio(contentMode: .fit).frame(width:20)
|
||||
Text("Settings").font(.body)
|
||||
}
|
||||
}
|
||||
})
|
||||
.buttonStyle(PlainButtonStyle()).shadow(color: .black.opacity(0.5), radius: 10)
|
||||
}
|
||||
|
||||
var hide: some View {
|
||||
BoringLargeButtons(
|
||||
action: {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
//vm.openMusic()
|
||||
}
|
||||
},
|
||||
icon: Image(systemName: "arrow.down.forward.and.arrow.up.backward"),
|
||||
title: "Hide"
|
||||
)
|
||||
}
|
||||
|
||||
var close: some View {
|
||||
BoringLargeButtons(
|
||||
action: {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: Image(systemName: "xmark"),
|
||||
title: "Exit"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
BoringExtrasMenu(vm: .init())
|
||||
}
|
||||
85
DynamicIsland/components/Notch/DynamicIslandHeader.swift
Normal file
85
DynamicIsland/components/Notch/DynamicIslandHeader.swift
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// DynamicIslandHeader.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created by Harsh Vardhan Goswami on 04/08/24.
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct DynamicIslandHeader: View {
|
||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||
@ObservedObject var batteryModel = BatteryStatusViewModel.shared
|
||||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||
@StateObject var tvm = TrayDrop.shared
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
HStack {
|
||||
if (!tvm.isEmpty || coordinator.alwaysShowTabs) && Defaults[.dynamicShelf] {
|
||||
TabSelectionView()
|
||||
} else if vm.notchState == .open {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.opacity(vm.notchState == .closed ? 0 : 1)
|
||||
.blur(radius: vm.notchState == .closed ? 20 : 0)
|
||||
.animation(.smooth.delay(0.1), value: vm.notchState)
|
||||
.zIndex(2)
|
||||
|
||||
if vm.notchState == .open {
|
||||
Rectangle()
|
||||
.fill(NSScreen.screens
|
||||
.first(where: { $0.localizedName == coordinator.selectedScreen })?.safeAreaInsets.top ?? 0 > 0 ? .black : .clear)
|
||||
.frame(width: vm.closedNotchSize.width)
|
||||
.mask {
|
||||
NotchShape()
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 4) {
|
||||
if vm.notchState == .open {
|
||||
if Defaults[.settingsIconInNotch] {
|
||||
SettingsLink(label: {
|
||||
Capsule()
|
||||
.fill(.black)
|
||||
.frame(width: 30, height: 30)
|
||||
.overlay {
|
||||
Image(systemName: "gear")
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
.imageScale(.medium)
|
||||
}
|
||||
})
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
if Defaults[.showBatteryIndicator] {
|
||||
DynamicIslandBatteryView(
|
||||
batteryWidth: 30,
|
||||
isCharging: batteryModel.isCharging,
|
||||
isInLowPowerMode: batteryModel.isInLowPowerMode,
|
||||
isPluggedIn: batteryModel.isPluggedIn,
|
||||
levelBattery: batteryModel.levelBattery,
|
||||
maxCapacity: batteryModel.maxCapacity,
|
||||
timeToFullCharge: batteryModel.timeToFullCharge,
|
||||
isForNotification: false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.system(.headline, design: .rounded))
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.opacity(vm.notchState == .closed ? 0 : 1)
|
||||
.blur(radius: vm.notchState == .closed ? 20 : 0)
|
||||
.animation(.smooth.delay(0.1), value: vm.notchState)
|
||||
.zIndex(2)
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
.environmentObject(vm)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
DynamicIslandHeader().environmentObject(DynamicIslandViewModel())
|
||||
}
|
||||
50
DynamicIsland/components/Notch/DynamicIslandWindow.swift
Normal file
50
DynamicIsland/components/Notch/DynamicIslandWindow.swift
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// DynamicIslandWindow.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created by Harsh Vardhan Goswami on 06/08/24.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class DynamicIslandWindow: NSPanel {
|
||||
override init(
|
||||
contentRect: NSRect,
|
||||
styleMask: NSWindow.StyleMask,
|
||||
backing: NSWindow.BackingStoreType,
|
||||
defer flag: Bool
|
||||
) {
|
||||
super.init(
|
||||
contentRect: contentRect,
|
||||
styleMask: styleMask,
|
||||
backing: backing,
|
||||
defer: flag
|
||||
)
|
||||
|
||||
isFloatingPanel = true
|
||||
isOpaque = false
|
||||
titleVisibility = .hidden
|
||||
titlebarAppearsTransparent = true
|
||||
backgroundColor = .clear
|
||||
isMovable = false
|
||||
|
||||
collectionBehavior = [
|
||||
.fullScreenAuxiliary,
|
||||
.stationary,
|
||||
.canJoinAllSpaces,
|
||||
.ignoresCycle,
|
||||
]
|
||||
|
||||
isReleasedWhenClosed = false
|
||||
level = .mainMenu + 3
|
||||
hasShadow = false
|
||||
}
|
||||
|
||||
override var canBecomeKey: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
override var canBecomeMain: Bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
342
DynamicIsland/components/Notch/NotchHomeView.swift
Normal file
342
DynamicIsland/components/Notch/NotchHomeView.swift
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
//
|
||||
// NotchHomeView.swift
|
||||
// boringNotch
|
||||
//
|
||||
// Created by Hugo Persson on 2024-08-18.
|
||||
// Modified by Harsh Vardhan Goswami & Richard Kunkli
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Music Player Components
|
||||
|
||||
struct MusicPlayerView: View {
|
||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||
let albumArtNamespace: Namespace.ID
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
AlbumArtView(vm: vm, albumArtNamespace: albumArtNamespace)
|
||||
MusicControlsView().drawingGroup().compositingGroup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AlbumArtView: View {
|
||||
@ObservedObject var musicManager = MusicManager.shared
|
||||
@ObservedObject var vm: DynamicIslandViewModel
|
||||
let albumArtNamespace: Namespace.ID
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
if Defaults[.lightingEffect] {
|
||||
albumArtBackground
|
||||
}
|
||||
albumArtButton
|
||||
}
|
||||
}
|
||||
|
||||
private var albumArtBackground: some View {
|
||||
Color.clear
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.background(
|
||||
Image(nsImage: musicManager.albumArt)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
)
|
||||
.clipped()
|
||||
.clipShape(RoundedRectangle(cornerRadius: Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.opened : MusicPlayerImageSizes.cornerRadiusInset.closed))
|
||||
.scaleEffect(x: 1.3, y: 1.4)
|
||||
.rotationEffect(.degrees(92))
|
||||
.blur(radius: 35)
|
||||
.opacity(min(0.6, 1 - max(musicManager.albumArt.getBrightness(), 0.3)))
|
||||
}
|
||||
|
||||
private var albumArtButton: some View {
|
||||
Button {
|
||||
musicManager.openMusicApp()
|
||||
} label: {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
albumArtImage
|
||||
appIconOverlay
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.opacity(musicManager.isPlaying ? 1 : 0.4)
|
||||
.scaleEffect(musicManager.isPlaying ? 1 : 0.85)
|
||||
}
|
||||
|
||||
private var albumArtImage: some View {
|
||||
Color.clear
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.background(
|
||||
Image(nsImage: musicManager.albumArt)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: musicManager.isFlipping)
|
||||
)
|
||||
.clipped()
|
||||
.clipShape(RoundedRectangle(cornerRadius: Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.opened : MusicPlayerImageSizes.cornerRadiusInset.closed))
|
||||
.matchedGeometryEffect(id: "albumArt", in: albumArtNamespace)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var appIconOverlay: some View {
|
||||
if vm.notchState == .open && !musicManager.usingAppIconForArtwork {
|
||||
AppIcon(for: musicManager.bundleIdentifier ?? "com.apple.Music")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 30, height: 30)
|
||||
.offset(x: 10, y: 10)
|
||||
.transition(.scale.combined(with: .opacity).animation(.bouncy.delay(0.3)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MusicControlsView: View {
|
||||
@ObservedObject var musicManager = MusicManager.shared
|
||||
@State private var sliderValue: Double = 0
|
||||
@State private var dragging: Bool = false
|
||||
@State private var lastDragged: Date = .distantPast
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
songInfoAndSlider
|
||||
playbackControls
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.frame(minWidth: Defaults[.showMirror] && Defaults[.showCalendar] ? 140 : 180)
|
||||
}
|
||||
|
||||
private var songInfoAndSlider: some View {
|
||||
GeometryReader { geo in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
songInfo(width: geo.size.width)
|
||||
musicSlider
|
||||
}
|
||||
}
|
||||
.padding(.top, 10)
|
||||
.padding(.leading, 5)
|
||||
}
|
||||
|
||||
private func songInfo(width: CGFloat) -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
MarqueeText($musicManager.songTitle, font: .headline, nsFont: .headline, textColor: .white, frameWidth: width)
|
||||
MarqueeText(
|
||||
$musicManager.artistName,
|
||||
font: .headline,
|
||||
nsFont: .headline,
|
||||
textColor: Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor)
|
||||
.ensureMinimumBrightness(factor: 0.6) : .gray,
|
||||
frameWidth: width
|
||||
)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}
|
||||
|
||||
private var musicSlider: some View {
|
||||
TimelineView(.animation(minimumInterval: musicManager.playbackRate > 0 ? 0.1 : nil)) { timeline in
|
||||
MusicSliderView(
|
||||
sliderValue: $sliderValue,
|
||||
duration: $musicManager.songDuration,
|
||||
lastDragged: $lastDragged,
|
||||
color: musicManager.avgColor,
|
||||
dragging: $dragging,
|
||||
currentDate: timeline.date,
|
||||
lastUpdated: musicManager.lastUpdated,
|
||||
ignoreLastUpdated: musicManager.ignoreLastUpdated,
|
||||
timestampDate: musicManager.timestampDate,
|
||||
elapsedTime: musicManager.elapsedTime,
|
||||
playbackRate: musicManager.playbackRate,
|
||||
isPlaying: musicManager.isPlaying
|
||||
) { newValue in
|
||||
MusicManager.shared.seek(to: newValue)
|
||||
}
|
||||
.padding(.top, 5)
|
||||
.frame(height: 36)
|
||||
}
|
||||
}
|
||||
|
||||
private var playbackControls: some View {
|
||||
HStack(spacing: 8) {
|
||||
HoverButton(icon: "backward.fill", scale: .medium) {
|
||||
MusicManager.shared.previousTrack()
|
||||
}
|
||||
HoverButton(icon: musicManager.isPlaying ? "pause.fill" : "play.fill", scale: .large) {
|
||||
MusicManager.shared.togglePlay()
|
||||
}
|
||||
HoverButton(icon: "forward.fill", scale: .medium) {
|
||||
MusicManager.shared.nextTrack()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Main View
|
||||
|
||||
struct NotchHomeView: View {
|
||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||
@EnvironmentObject var webcamManager: WebcamManager
|
||||
@ObservedObject var batteryModel = BatteryStatusViewModel.shared
|
||||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||
let albumArtNamespace: Namespace.ID
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if !coordinator.firstLaunch {
|
||||
mainContent
|
||||
}
|
||||
}
|
||||
.transition(.opacity.combined(with: .blurReplace))
|
||||
}
|
||||
|
||||
private var mainContent: some View {
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
MusicPlayerView(albumArtNamespace: albumArtNamespace)
|
||||
|
||||
if Defaults[.showCalendar] {
|
||||
CalendarView()
|
||||
.onHover { isHovering in
|
||||
vm.isHoveringCalendar = isHovering
|
||||
}
|
||||
.environmentObject(vm)
|
||||
}
|
||||
|
||||
if Defaults[.showMirror] && webcamManager.cameraAvailable {
|
||||
CameraPreviewView(webcamManager: webcamManager)
|
||||
.scaledToFit()
|
||||
.opacity(vm.notchState == .closed ? 0 : 1)
|
||||
.blur(radius: vm.notchState == .closed ? 20 : 0)
|
||||
}
|
||||
}
|
||||
.transition(.opacity.animation(.smooth.speed(0.9))
|
||||
.combined(with: .blurReplace.animation(.smooth.speed(0.9)))
|
||||
.combined(with: .move(edge: .top)))
|
||||
.blur(radius: vm.notchState == .closed ? 30 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
struct MusicSliderView: View {
|
||||
@Binding var sliderValue: Double
|
||||
@Binding var duration: Double
|
||||
@Binding var lastDragged: Date
|
||||
var color: NSColor
|
||||
@Binding var dragging: Bool
|
||||
let currentDate: Date
|
||||
let lastUpdated: Date
|
||||
let ignoreLastUpdated: Bool
|
||||
let timestampDate: Date
|
||||
let elapsedTime: Double
|
||||
let playbackRate: Double
|
||||
let isPlaying: Bool
|
||||
var onValueChange: (Double) -> Void
|
||||
|
||||
var currentElapsedTime: Double {
|
||||
// A small buffer is needed to ensure a meaningful difference between the two dates
|
||||
guard !dragging, timestampDate.timeIntervalSince(lastDragged) > -0.0001, (timestampDate > lastUpdated || ignoreLastUpdated) else { return sliderValue }
|
||||
let timeDifference = isPlaying ? currentDate.timeIntervalSince(timestampDate) : 0
|
||||
let elapsed = elapsedTime + (timeDifference * playbackRate)
|
||||
return min(elapsed, duration)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
CustomSlider(
|
||||
value: $sliderValue,
|
||||
range: 0 ... duration,
|
||||
color: Defaults[.sliderColor] == SliderColorEnum.albumArt ? Color(
|
||||
nsColor: color
|
||||
).ensureMinimumBrightness(factor: 0.8) : Defaults[.sliderColor] == SliderColorEnum.accent ? Defaults[.accentColor] : .white,
|
||||
dragging: $dragging,
|
||||
lastDragged: $lastDragged,
|
||||
onValueChange: onValueChange
|
||||
)
|
||||
.frame(height: 10, alignment: .center)
|
||||
HStack {
|
||||
Text(timeString(from: sliderValue))
|
||||
Spacer()
|
||||
Text(timeString(from: duration))
|
||||
}
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: color)
|
||||
.ensureMinimumBrightness(factor: 0.6) : .gray)
|
||||
.font(.caption)
|
||||
}
|
||||
.onChange(of: currentDate) {
|
||||
sliderValue = currentElapsedTime
|
||||
}
|
||||
}
|
||||
|
||||
func timeString(from seconds: Double) -> String {
|
||||
let totalMinutes = Int(seconds) / 60
|
||||
let remainingSeconds = Int(seconds) % 60
|
||||
let hours = totalMinutes / 60
|
||||
let minutes = totalMinutes % 60
|
||||
|
||||
if hours > 0 {
|
||||
return String(format: "%d:%02d:%02d", hours, minutes, remainingSeconds)
|
||||
} else {
|
||||
return String(format: "%d:%02d", minutes, remainingSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomSlider: View {
|
||||
@Binding var value: Double
|
||||
var range: ClosedRange<Double>
|
||||
var color: Color = .white
|
||||
@Binding var dragging: Bool
|
||||
@Binding var lastDragged: Date
|
||||
var onValueChange: ((Double) -> Void)?
|
||||
var thumbSize: CGFloat = 12
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
let width = geometry.size.width
|
||||
let height = CGFloat(dragging ? 9 : 5)
|
||||
let rangeSpan = range.upperBound - range.lowerBound
|
||||
|
||||
let filledTrackWidth = min(rangeSpan == .zero ? 0 : ((value - range.lowerBound) / rangeSpan) * width, width)
|
||||
|
||||
ZStack(alignment: .leading) {
|
||||
// Background track
|
||||
Rectangle()
|
||||
.fill(.gray.opacity(0.3))
|
||||
.frame(height: height)
|
||||
|
||||
// Filled track
|
||||
Rectangle()
|
||||
.fill(color)
|
||||
.frame(width: filledTrackWidth, height: height)
|
||||
}
|
||||
.cornerRadius(height / 2)
|
||||
.frame(height: 10)
|
||||
.contentShape(Rectangle())
|
||||
.highPriorityGesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { gesture in
|
||||
withAnimation {
|
||||
dragging = true
|
||||
}
|
||||
let newValue = range.lowerBound + Double(gesture.location.x / width) * rangeSpan
|
||||
value = min(max(newValue, range.lowerBound), range.upperBound)
|
||||
}
|
||||
.onEnded { _ in
|
||||
onValueChange?(value)
|
||||
dragging = false
|
||||
lastDragged = Date()
|
||||
}
|
||||
)
|
||||
.animation(.bouncy.speed(1.4), value: dragging)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NotchHomeView(albumArtNamespace: Namespace().wrappedValue)
|
||||
.environmentObject(DynamicIslandViewModel())
|
||||
.environmentObject(WebcamManager())
|
||||
}
|
||||
126
DynamicIsland/components/Notch/NotchShape.swift
Normal file
126
DynamicIsland/components/Notch/NotchShape.swift
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
//
|
||||
// NotchShape.swift
|
||||
// boringNotch
|
||||
//
|
||||
// Created by Kai Azim on 2023-08-24.
|
||||
// Original source: https://github.com/MrKai77/DynamicNotchKit
|
||||
// Modified by Alexander Greco on 2025-05-18.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NotchShape: Shape {
|
||||
private var topCornerRadius: CGFloat
|
||||
private var bottomCornerRadius: CGFloat
|
||||
|
||||
init(
|
||||
topCornerRadius: CGFloat? = nil,
|
||||
bottomCornerRadius: CGFloat? = nil
|
||||
) {
|
||||
self.topCornerRadius = topCornerRadius ?? 6
|
||||
self.bottomCornerRadius = bottomCornerRadius ?? 14
|
||||
}
|
||||
|
||||
var animatableData: AnimatablePair<CGFloat, CGFloat> {
|
||||
get {
|
||||
.init(
|
||||
topCornerRadius,
|
||||
bottomCornerRadius
|
||||
)
|
||||
}
|
||||
set {
|
||||
topCornerRadius = newValue.first
|
||||
bottomCornerRadius = newValue.second
|
||||
}
|
||||
}
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
|
||||
path.move(
|
||||
to: CGPoint(
|
||||
x: rect.minX,
|
||||
y: rect.minY
|
||||
)
|
||||
)
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(
|
||||
x: rect.minX + topCornerRadius,
|
||||
y: rect.minY + topCornerRadius
|
||||
),
|
||||
control: CGPoint(
|
||||
x: rect.minX + topCornerRadius,
|
||||
y: rect.minY
|
||||
)
|
||||
)
|
||||
|
||||
path.addLine(
|
||||
to: CGPoint(
|
||||
x: rect.minX + topCornerRadius,
|
||||
y: rect.maxY - bottomCornerRadius
|
||||
)
|
||||
)
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(
|
||||
x: rect.minX + topCornerRadius + bottomCornerRadius,
|
||||
y: rect.maxY
|
||||
),
|
||||
control: CGPoint(
|
||||
x: rect.minX + topCornerRadius,
|
||||
y: rect.maxY
|
||||
)
|
||||
)
|
||||
|
||||
path.addLine(
|
||||
to: CGPoint(
|
||||
x: rect.maxX - topCornerRadius - bottomCornerRadius,
|
||||
y: rect.maxY
|
||||
)
|
||||
)
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(
|
||||
x: rect.maxX - topCornerRadius,
|
||||
y: rect.maxY - bottomCornerRadius
|
||||
),
|
||||
control: CGPoint(
|
||||
x: rect.maxX - topCornerRadius,
|
||||
y: rect.maxY
|
||||
)
|
||||
)
|
||||
|
||||
path.addLine(
|
||||
to: CGPoint(
|
||||
x: rect.maxX - topCornerRadius,
|
||||
y: rect.minY + topCornerRadius
|
||||
)
|
||||
)
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(
|
||||
x: rect.maxX,
|
||||
y: rect.minY
|
||||
),
|
||||
control: CGPoint(
|
||||
x: rect.maxX - topCornerRadius,
|
||||
y: rect.minY
|
||||
)
|
||||
)
|
||||
|
||||
path.addLine(
|
||||
to: CGPoint(
|
||||
x: rect.minX,
|
||||
y: rect.minY
|
||||
)
|
||||
)
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NotchShape(topCornerRadius: 6, bottomCornerRadius: 14)
|
||||
.frame(width: 200, height: 32)
|
||||
.padding(10)
|
||||
}
|
||||
64
DynamicIsland/components/Notch/NotchShelfView.swift
Normal file
64
DynamicIsland/components/Notch/NotchShelfView.swift
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct NotchShelfView: View {
|
||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||
@StateObject var tvm = TrayDrop.shared
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
AirDropView()
|
||||
panel
|
||||
.onDrop(of: [.data], isTargeted: $vm.dropZoneTargeting) { providers in
|
||||
vm.dropEvent = true
|
||||
DispatchQueue.global().async {
|
||||
tvm.load(providers)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var panel: some View {
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.strokeBorder(style: StrokeStyle(lineWidth: 4, dash: [10]))
|
||||
.foregroundStyle(.white.opacity(0.1))
|
||||
.overlay {
|
||||
content
|
||||
.padding()
|
||||
}
|
||||
.animation(vm.animation, value: tvm.items)
|
||||
.animation(vm.animation, value: tvm.isLoading)
|
||||
}
|
||||
|
||||
var content: some View {
|
||||
Group {
|
||||
if tvm.isEmpty {
|
||||
VStack(spacing: 10) {
|
||||
Image(systemName: "tray.and.arrow.down")
|
||||
.symbolVariant(.fill)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundStyle(.white, .gray)
|
||||
.imageScale(.large)
|
||||
|
||||
Text("Drop files here")
|
||||
.foregroundStyle(.gray)
|
||||
.font(.system(.title3, design: .rounded))
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
} else {
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: spacing) {
|
||||
ForEach(tvm.items) { item in
|
||||
DropItemView(item: item)
|
||||
}
|
||||
}
|
||||
.padding(spacing)
|
||||
}
|
||||
.padding(-spacing)
|
||||
.scrollIndicators(.never)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue