Restored files from old Mac, force pushing

This commit is contained in:
Hariharan Mudaliar 2025-07-13 20:26:03 +05:30
commit 5040381840
126 changed files with 10431 additions and 67 deletions

View file

@ -0,0 +1,82 @@
//
// AnimatedFace.swift
//
// Created by Harsh Vardhan Goswami on 04/08/24.
//
import SwiftUI
struct MinimalFaceFeatures: View {
@State private var isBlinking = false
@State var height:CGFloat = 20;
@State var width:CGFloat = 30;
var body: some View {
VStack(spacing: 4) { // Adjusted spacing to fit within 30x30
// Eyes
HStack(spacing: 4) { // Adjusted spacing to fit within 30x30
Eye(isBlinking: $isBlinking)
Eye(isBlinking: $isBlinking)
}
// Nose and mouth combined
VStack(spacing: 2) { // Adjusted spacing to fit within 30x30
// Nose
RoundedRectangle(cornerRadius: 2)
.fill(Color.white)
.frame(width: 3, height: 4)
// Mouth (happy)
GeometryReader { geometry in
Path { path in
let width = geometry.size.width
let height = geometry.size.height
path.move(to: CGPoint(x: 0, y: height / 2))
path.addQuadCurve(to: CGPoint(x: width, y: height / 2), control: CGPoint(x: width / 2, y: height))
}
.stroke(Color.white, lineWidth: 2)
}
.frame(width: 14, height: 10)
}
}
.frame(width: self.width, height: self.height) // Maximum size of face
.onAppear {
startBlinking()
}
}
func startBlinking() {
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
withAnimation(.spring(duration: 0.2)) {
isBlinking = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation(.spring(duration: 0.2)) {
isBlinking = false
}
}
}
}
}
struct Eye: View {
@Binding var isBlinking: Bool
var body: some View {
RoundedRectangle(cornerRadius: 10)
.fill(Color.white)
.frame(width: 4, height: isBlinking ? 1 : 4)
.frame(maxWidth: 15, maxHeight: 15) // Adjusted max size
.animation(.easeInOut(duration: 0.1), value: isBlinking)
}
}
struct MinimalFaceFeatures_Previews: PreviewProvider {
static var previews: some View {
ZStack {
Color.black
MinimalFaceFeatures()
}
.previewLayout(.fixed(width: 60, height: 60)) // Adjusted preview size for better visibility
}
}

View file

@ -0,0 +1,39 @@
import SwiftUI
struct BottomRoundedRectangle: Shape {
var radius: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
// Top left corner
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
// Top right corner
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
// Bottom right corner (rounded)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - radius))
path.addArc(center: CGPoint(x: rect.maxX - radius, y: rect.maxY - radius),
radius: radius,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 90),
clockwise: false)
// Bottom left corner (rounded)
path.addLine(to: CGPoint(x: rect.minX + radius, y: rect.maxY))
path.addArc(center: CGPoint(x: rect.minX + radius, y: rect.maxY - radius),
radius: radius,
startAngle: Angle(degrees: 90),
endAngle: Angle(degrees: 180),
clockwise: false)
// Back to top left to close the path
path.closeSubpath()
return path
}
}

View file

@ -0,0 +1,315 @@
//
// BoringCalender.swift
// boringNotch
//
// Created by Harsh Vardhan Goswami on 08/09/24.
//
import EventKit
import SwiftUI
import Defaults
struct Config: Equatable {
// var count: Int = 10 // 3 days past + today + 7 days future
var past: Int = 3
var future: Int = 7
var steps: Int = 1 // Each step is one day
var spacing: CGFloat = 1
var showsText: Bool = true
var offset: Int = 2 // Number of dates to the left of the selected date
}
struct WheelPicker: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@Binding var selectedDate: Date
@State private var scrollPosition: Int?
@State private var haptics: Bool = false
@State private var byClick: Bool = false
let config: Config
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: config.spacing) {
let totalSteps = config.steps * (config.past + config.future)
let spacerNum = config.offset
ForEach(0..<totalSteps + 2 * spacerNum + 1, id: \.self) { index in
if(index < spacerNum || index > totalSteps + spacerNum - 1){
Spacer().frame(width: 24, height: 24).id(index)
} else {
let offset = -config.offset - config.past
let date = dateForIndex(index, offset: offset)
let isSelected = isDateSelected(index, offset: offset)
dateButton(date: date, isSelected: isSelected, offset: offset){
selectedDate = date
byClick = true
withAnimation{
scrollPosition = indexForDate(date, offset: offset) - config.offset
}
//haptics.toggle() // Causes double haptic when click
}
}
}
}
.frame(height: 50)
.scrollTargetLayout()
}
.scrollIndicators(.never)
.scrollPosition(id: $scrollPosition, anchor: .leading)
.scrollTargetBehavior(.viewAligned) // Ensures scroll view snaps to button center
.safeAreaPadding(.horizontal)
.sensoryFeedback(.alignment, trigger: haptics)
.onChange(of: scrollPosition) { oldValue, newValue in
if(!byClick){
handleScrollChange(newValue: newValue, config: config)
}else{
byClick = false
}
}
.onAppear {
scrollToToday(config: config)
}
}
private func dateButton(date: Date, isSelected: Bool, offset: Int, onClick:@escaping()->Void) -> some View {
Button(action: onClick) {
VStack(spacing: 2) {
dayText(date: dateToString(for: date), isSelected: isSelected)
dateCircle(date: date, isSelected: isSelected)
}
}
.buttonStyle(PlainButtonStyle())
.id(indexForDate(date, offset: offset))
}
private func dayText(date: String, isSelected: Bool) -> some View {
Text(date)
.font(.caption2)
.foregroundStyle(
isSelected ? Defaults[.accentColor] : .gray
)
}
private func dateCircle(date: Date, isSelected: Bool) -> some View {
ZStack {
Circle()
.fill(isSelected ? Defaults[.accentColor] : .clear)
.frame(width: 24, height: 24)
Text("\(date.date)")
.font(.title3)
.foregroundStyle(isSelected ? .white : .gray)
}
}
func handleScrollChange(newValue: Int?, config: Config) {
let offset = -config.offset - config.past
let todayIndex = indexForDate(Date(), offset: offset)
guard let newIndex = newValue else { return }
let targetDateIndex = newIndex + config.offset
switch targetDateIndex{
case todayIndex-config.past..<todayIndex+config.future:
selectedDate = dateForIndex(targetDateIndex, offset: offset)
haptics.toggle()
default:
return
}
}
private func scrollToToday(config: Config) {
let today = Date()
let todayIndex = indexForDate(today, offset: -config.offset - config.past)
byClick = true
scrollPosition = todayIndex - config.offset
selectedDate = today
}
private func indexForDate(_ date: Date, offset: Int) -> Int {
let calendar = Calendar.current
let startDate = calendar.startOfDay(for: calendar.date(byAdding: .day, value: offset, to: Date()) ?? Date())
let targetDate = calendar.startOfDay(for: date)
let daysDifference = calendar.dateComponents([.day], from: startDate, to: targetDate).day ?? 0
return daysDifference
}
private func dateForIndex(_ index: Int, offset: Int) -> Date {
let startDate = Calendar.current.date(byAdding: .day, value: offset, to: Date()) ?? Date()
return Calendar.current.date(byAdding: .day, value: index, to: startDate) ?? Date()
}
private func dayForIndex(_ index: Int, offset: Int) -> String {
let date = dateForIndex(index, offset: offset)
return dateToString(for: date)
}
private func dateToString(for date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "E"
return formatter.string(from: date)
}
private func isDateSelected(_ index: Int, offset: Int) -> Bool {
Calendar.current.isDate(dateForIndex(index, offset: offset), inSameDayAs: selectedDate)
}
}
struct CalendarView: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@StateObject private var calendarManager = CalendarManager()
@State private var selectedDate = Date()
var body: some View {
VStack(spacing: 8) {
HStack {
Text("\(selectedDate, format: .dateTime.month())")
.font(.system(size: 18))
.fontWeight(.semibold)
ZStack {
WheelPicker(selectedDate: $selectedDate, config: Config())
HStack {
LinearGradient(
colors: [.black, .clear], startPoint: .leading, endPoint: .trailing
)
.frame(width: 20)
Spacer()
LinearGradient(
colors: [.clear, .black], startPoint: .leading, endPoint: .trailing
)
.frame(width: 20)
}
}
}
if calendarManager.events.isEmpty {
EmptyEventsView()
} else {
EventListView(events: calendarManager.events)
}
}
.listRowBackground(Color.clear)
.onChange(of: selectedDate) { _, newDate in
calendarManager.updateCurrentDate(newDate)
}
.onChange(of: vm.notchState) { _, _ in
calendarManager.updateCurrentDate(Date.now)
}
.onAppear {
calendarManager.updateCurrentDate(Date.now)
}
}
}
struct EmptyEventsView: View {
var body: some View {
ScrollView {
Text("No events today")
.font(.headline)
.foregroundStyle(.white)
Text("Enjoy your free time!")
.font(.subheadline)
.foregroundColor(.gray)
}
}
}
struct EventListView: View {
@Environment(\.openURL) private var openURL
let events: [EKEvent]
var body: some View {
ScrollView(showsIndicators: false) {
HStack(alignment: .top) {
VStack(alignment: .trailing, spacing: 5) {
ForEach(events.indices, id: \.self) { index in
VStack(alignment: .trailing) {
if isAllDayEvent(
start: events[index].startDate, end: events[index].endDate)
{
Text("All-day")
} else {
Text("\(events[index].startDate, style: .time)")
Text("\(events[index].endDate, style: .time)")
}
}
.multilineTextAlignment(.trailing)
.padding(.bottom, 8)
.font(.caption2)
}
}
VStack(alignment: .leading, spacing: 5) {
ForEach(events.indices, id: \.self) { index in
HStack(alignment: .top) {
VStack(spacing: 5) {
Image(
systemName: isEventEnded(events[index].endDate)
? "checkmark.circle" : "circle"
)
.foregroundColor(isEventEnded(events[index].endDate) ? .green : .gray)
.font(.footnote)
Rectangle()
.frame(width: 1)
.foregroundStyle(.gray.opacity(0.5))
.opacity(index == events.count - 1 ? 0 : 1)
}
.padding(.top, 1)
Button {
if let url = generateEventURL(for: events[index]) {
openURL(url)
}
} label: {
Text(events[index].title)
.font(.footnote)
.foregroundStyle(.gray)
}
.buttonStyle(.plain)
Spacer(minLength: 0)
}
.opacity(isEventEnded(events[index].endDate) ? 0.6 : 1)
}
}
}
}
.scrollIndicators(.never)
.scrollTargetBehavior(.viewAligned)
}
private func isAllDayEvent(start: Date, end: Date) -> Bool {
let calendar = Calendar.current
let startComponents = calendar.dateComponents([.hour, .minute], from: start)
let endComponents = calendar.dateComponents([.hour, .minute], from: end)
return startComponents.hour == 0 && startComponents.minute == 0 && endComponents.hour == 23
&& endComponents.minute == 59
}
private func isEventEnded(_ end: Date) -> Bool {
return Date.now > end
}
private func generateEventURL(for event: EKEvent) -> URL? {
var dateComponent = ""
if event.hasRecurrenceRules {
if let startDate = event.startDate {
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
formatter.timeZone = TimeZone.current
if !event.isAllDay {
formatter.timeZone = TimeZone(secondsFromGMT: 0)
}
dateComponent = "/\(formatter.string(from: startDate))"
}
}
return URL(string: "ical://ekevent\(dateComponent)/\(event.calendarItemIdentifier)?method=show&options=more")
}
}
// Keep the CalendarManager, EmptyEventsView, and EventListView as they were in the previous implementation
#Preview {
CalendarView()
.frame(width: 250)
.padding(.horizontal)
.background(.black)
.environmentObject(DynamicIslandViewModel())
}

View file

@ -0,0 +1,109 @@
//
// DynamicIslandSystemTiles.swift
// DynamicIsland
//
// Created by Harsh Vardhan Goswami on 04/08/24.
// BoringSystemTiles.swift
// boringNotch
//
// Created by Harsh Vardhan Goswami on 16/08/24.
//
import Foundation
import SwiftUI
import Defaults
struct SystemItemButton: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@State var icon: String = "gear"
var onTap: () -> Void
@State var label: String?
@State var showEmojis: Bool = true
@State var emoji: String = "🔧"
var body: some View {
Button(action: onTap) {
if Defaults[.tileShowLabels] {
HStack {
if !showEmojis {
Image(systemName: icon)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 10)
.foregroundStyle(.gray)
}
Text((showEmojis ? "\(emoji) " : "") + label!)
.font(.caption2)
.fontWeight(.regular)
.foregroundStyle(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
.allowsTightening(true)
.minimumScaleFactor(0.7)
.lineLimit(1)
}
} else {
Color.clear
.overlay {
Image(systemName: icon)
.foregroundStyle(.gray)
}
.aspectRatio(1, contentMode: .fit)
}
}
.buttonStyle(BouncingButtonStyle(vm: vm))
}
}
func logout() {
DispatchQueue.global(qos: .background).async {
let appleScript = """
tell application "System Events" to log out
"""
var error: NSDictionary?
if let scriptObject = NSAppleScript(source: appleScript) {
scriptObject.executeAndReturnError(&error)
if let error = error {
print("Error: \(error)")
}
}
}
}
struct DynamicIslandSystemTiles: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
struct ItemButton {
var icon: String
var onTap: () -> Void
}
var body: some View {
Grid {
GridRow {
// SystemItemButton(icon: "clipboard", onTap: {
// vm.openClipboard()
// }, label: "Clipboard History", showEmojis: Defaults[.showEmojis], emoji: "✨")
// SystemItemButton(icon: "keyboard", onTap: {
// vm?.close()
// vm?.togglesneakPeek(status: true, type: .backlight, value: 1)
// }, label: "💡 Keyboard Backlight")
}
GridRow {
SystemItemButton(icon: coordinator.currentMicStatus ? "mic" : "mic.slash", onTap: {
coordinator.toggleMic()
vm.close()
}, label: "Toggle Microphone", showEmojis: Defaults[.showEmojis], emoji: coordinator.currentMicStatus ? "😀" : "🤫")
// SystemItemButton(icon: "lock", onTap: {
// logout()
// }, label: "🔒 Lock My Device")
}
}
}
}
#Preview {
DynamicIslandSystemTiles().padding()
}

View file

@ -0,0 +1,26 @@
//
// EmptyState.swift
//
// Created by Harsh Vardhan Goswami on 04/08/24.
//
import SwiftUI
struct EmptyStateView: View {
var message: String
@State private var isVisible = true
var body: some View {
HStack {
MinimalFaceFeatures(
height: 70, width: 80)
Text(message)
.font(.system(size:14))
.foregroundColor(.gray)
}.transition(.blurReplace.animation(.spring(.bouncy(duration: 0.3)))) // Smooth animation
}
}
#Preview {
EmptyStateView(message: "Play some music babies")
}

View file

@ -0,0 +1,46 @@
//
// HoverButton.swift
// boringNotch
//
// Created by Kraigo on 04.09.2024.
//
import SwiftUI
struct HoverButton: View {
var icon: String
var iconColor: Color = .white;
var scale: Image.Scale = .medium
var action: () -> Void
var contentTransition: ContentTransition = .symbolEffect;
@State private var isHovering = false
var body: some View {
let size = CGFloat(scale == .large ? 40 : 30)
Button(action: action) {
Rectangle()
.fill(.clear)
.contentShape(Rectangle())
.frame(width: size, height: size)
.overlay {
Capsule()
.fill(isHovering ? Color.gray.opacity(0.2) : .clear)
.frame(width: size, height: size)
.overlay {
Image(systemName: icon)
.foregroundColor(iconColor)
.contentTransition(contentTransition)
.font(scale == .large ? .largeTitle : .body)
}
}
}
.buttonStyle(PlainButtonStyle())
.onHover { hovering in
withAnimation(.smooth(duration: 0.3)) {
isHovering = hovering
}
}
}
}

View file

@ -0,0 +1,52 @@
//
// DownloadView.swift
// boringNotch
//
// Created by Harsh Vardhan Goswami on 17/08/24.
//
import Foundation
import SwiftUI
enum Browser {
case safari
case chrome
}
struct DownloadFile {
var name: String
var size: Int
var formattedSize: String
var browser: Browser
}
class DownloadWatcher: ObservableObject {
@Published var downloadFiles: [DownloadFile] = []
}
struct DownloadArea: View {
@EnvironmentObject var watcher: DownloadWatcher
var body: some View {
HStack(alignment: .center) {
HStack {
if watcher.downloadFiles.first!.browser == .safari {
AppIcon(for: "com.apple.safari")
} else {
Image(.chrome).resizable().scaledToFit().frame(width: 30, height: 30)
}
VStack(alignment: .leading) {
Text("Download")
Text("In progress").font(.system(.footnote)).foregroundStyle(.gray)
}
}
Spacer()
HStack(spacing: 12) {
VStack(alignment: .trailing) {
Text(watcher.downloadFiles.first!.formattedSize)
Text(watcher.downloadFiles.first!.name).font(.caption2).foregroundStyle(.gray)
}
}
}
}
}

View file

@ -0,0 +1,256 @@
import SwiftUI
import Defaults
/// A view that displays the battery status with an icon and charging indicator.
struct BatteryView: View {
var levelBattery: Float
var isPluggedIn: Bool
var isCharging: Bool
var isInLowPowerMode: Bool
var batteryWidth: CGFloat = 26
var isForNotification: Bool
var animationStyle: BoringAnimations = BoringAnimations()
var icon: String = "battery.0"
/// Determines the icon to display when charging.
var iconStatus: String {
if isCharging {
return "bolt"
}
else if isPluggedIn {
return "plug"
}
else {
return ""
}
}
/// Determines the color of the battery based on its status.
var batteryColor: Color {
if isInLowPowerMode {
return .yellow
} else if levelBattery <= 20 && !isCharging && !isPluggedIn {
return .red
} else if isCharging || isPluggedIn || levelBattery == 100 {
return .green
} else {
return .white
}
}
var body: some View {
ZStack(alignment: .leading) {
Image(systemName: icon)
.resizable()
.fontWeight(.thin)
.aspectRatio(contentMode: .fit)
.foregroundColor(.white.opacity(0.5))
.frame(
width: batteryWidth + 1
)
RoundedRectangle(cornerRadius: 2.5)
.fill(batteryColor)
.frame(
width: CGFloat(((CGFloat(CFloat(levelBattery)) / 100) * (batteryWidth - 6))),
height: (batteryWidth - 2.75) - 18
)
.padding(.leading, 2)
if iconStatus != "" && (isForNotification || Defaults[.showPowerStatusIcons]) {
ZStack {
Image(iconStatus)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
.frame(
width: 17,
height: 17
)
}
.frame(width: batteryWidth, height: batteryWidth)
}
}
}
}
/// A view that displays detailed battery information and settings.
struct BatteryMenuView: View {
var isPluggedIn: Bool
var isCharging: Bool
var levelBattery: Float
var maxCapacity: Float
var timeToFullCharge: Int
var isInLowPowerMode: Bool
var onDismiss: () -> Void
@Environment(\.openURL) private var openURL
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("Battery Status")
.font(.headline)
.fontWeight(.semibold)
Spacer()
Text("\(Int(levelBattery))%")
.font(.headline)
.fontWeight(.semibold)
}
VStack(alignment: .leading, spacing: 8) {
Text("Max Capacity: \(Int(maxCapacity))%")
.font(.subheadline)
.fontWeight(.regular)
if isInLowPowerMode {
Label("Low Power Mode", systemImage: "bolt.circle")
.font(.subheadline)
.fontWeight(.regular)
}
if isCharging {
Label("Charging", systemImage: "bolt.fill")
.font(.subheadline)
.fontWeight(.regular)
}
if isPluggedIn {
Label("Plugged In", systemImage: "powerplug.fill")
.font(.subheadline)
.fontWeight(.regular)
}
if timeToFullCharge > 0 {
Label("Time to Full Charge: \(timeToFullCharge) min", systemImage: "clock")
.font(.subheadline)
.fontWeight(.regular)
}
if !isCharging && isPluggedIn && levelBattery >= 80 {
Label("Charging on Hold: Desktop Mode", systemImage: "desktopcomputer")
.font(.subheadline)
.fontWeight(.regular)
}
}
.padding(.vertical, 8)
Divider().background(Color.white)
Button(action: openBatteryPreferences) {
Label("Battery Settings", systemImage: "gearshape")
.fontWeight(.regular)
}
.frame(maxWidth: .infinity)
.buttonStyle(.plain)
.padding(.vertical, 8)
}
.padding()
.frame(width: 280)
.foregroundColor(.white)
}
private func openBatteryPreferences() {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.battery") {
openURL(url)
onDismiss()
}
}
}
/// A view that displays the battery status and allows interaction to show detailed information.
struct DynamicIslandBatteryView: View {
@State var batteryWidth: CGFloat = 26
var isCharging: Bool = false
var isInLowPowerMode: Bool = false
var isPluggedIn: Bool = false
var levelBattery: Float = 0
var maxCapacity: Float = 0
var timeToFullCharge: Int = 0
@State var isForNotification: Bool = false
@State private var showPopupMenu: Bool = false
@State private var isPressed: Bool = false
@State private var isHoveringPopover: Bool = false
@EnvironmentObject var vm: DynamicIslandViewModel
var body: some View {
HStack {
if Defaults[.showBatteryPercentage] {
Text("\(Int32(levelBattery))%")
.font(.callout)
.foregroundStyle(.white)
}
BatteryView(
levelBattery: levelBattery,
isPluggedIn: isPluggedIn,
isCharging: isCharging,
isInLowPowerMode: isInLowPowerMode,
batteryWidth: batteryWidth,
isForNotification: isForNotification
)
}
.scaleEffect(isPressed ? 0.95 : 1.0)
.animation(.easeInOut(duration: 0.2), value: isPressed)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
withAnimation {
isPressed = true
}
}
.onEnded { _ in
withAnimation {
isPressed = false
showPopupMenu.toggle()
}
}
)
.popover(
isPresented: $showPopupMenu,
arrowEdge: .bottom) {
BatteryMenuView(
isPluggedIn: isPluggedIn,
isCharging: isCharging,
levelBattery: levelBattery,
maxCapacity: maxCapacity,
timeToFullCharge: timeToFullCharge,
isInLowPowerMode: isInLowPowerMode,
onDismiss: {
showPopupMenu = false
}
)
.onHover { hovering in
isHoveringPopover = hovering
}
}
.onChange(of: showPopupMenu) { _, _ in
updateBatteryPopoverActiveState()
}
.onChange(of: isHoveringPopover) { _, _ in
updateBatteryPopoverActiveState()
}
}
private func updateBatteryPopoverActiveState() {
vm.isBatteryPopoverActive = showPopupMenu && isHoveringPopover
}
}
#Preview {
DynamicIslandBatteryView(
batteryWidth: 30,
isCharging: false,
isInLowPowerMode: false,
isPluggedIn: true,
levelBattery: 80,
maxCapacity: 100,
timeToFullCharge: 10,
isForNotification: false
).frame(width: 200, height: 200)
}

View file

@ -0,0 +1,147 @@
//
// InlineHUDs.swift
// boringNotch
//
// Created by Richard Kunkli on 14/09/2024.
//
import SwiftUI
struct InlineHUD: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@Binding var type: SneakContentType
@Binding var value: CGFloat
@Binding var icon: String
@Binding var hoverAnimation: Bool
@Binding var gestureProgress: CGFloat
var body: some View {
HStack {
HStack(spacing: 5) {
Group {
switch (type) {
case .volume:
if icon.isEmpty {
Image(systemName: SpeakerSymbol(value))
.contentTransition(.interpolate)
.symbolVariant(value > 0 ? .none : .slash)
.frame(width: 20, height: 15, alignment: .leading)
} else {
Image(systemName: icon)
.contentTransition(.interpolate)
.opacity(value.isZero ? 0.6 : 1)
.scaleEffect(value.isZero ? 0.85 : 1)
.frame(width: 20, height: 15, alignment: .leading)
}
case .brightness:
Image(systemName: BrightnessSymbol(value))
.contentTransition(.interpolate)
.frame(width: 20, height: 15, alignment: .center)
case .backlight:
Image(systemName: "keyboard")
.contentTransition(.interpolate)
.frame(width: 20, height: 15, alignment: .center)
case .mic:
Image(systemName: "mic")
.symbolRenderingMode(.hierarchical)
.symbolVariant(value > 0 ? .none : .slash)
.contentTransition(.interpolate)
.frame(width: 20, height: 15, alignment: .center)
default:
EmptyView()
}
}
.foregroundStyle(.white)
.symbolVariant(.fill)
Text(Type2Name(type))
.font(.subheadline)
.fontWeight(.medium)
.lineLimit(1)
.allowsTightening(true)
.contentTransition(.numericText())
}
.frame(width: 100 - (hoverAnimation ? 0 : 12) + gestureProgress / 2, height: vm.notchSize.height - (hoverAnimation ? 0 : 12), alignment: .leading)
Rectangle()
.fill(.black)
.frame(width: vm.closedNotchSize.width - 20)
HStack {
if (type == .mic) {
Text(value.isZero ? "muted" : "unmuted")
.foregroundStyle(.gray)
.lineLimit(1)
.allowsTightening(true)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity, alignment: .trailing)
.contentTransition(.interpolate)
} else {
HStack {
DraggableProgressBar(value: $value)
if (type == .volume && value.isZero) {
Text("muted")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.gray)
.lineLimit(1)
.allowsTightening(true)
.multilineTextAlignment(.trailing)
}
}
}
}
.padding(.trailing, 4)
.frame(width: 100 - (hoverAnimation ? 0 : 12) + gestureProgress / 2, height: vm.closedNotchSize.height - (hoverAnimation ? 0 : 12), alignment: .center)
}
.frame(height: vm.closedNotchSize.height + (hoverAnimation ? 8 : 0), alignment: .center)
}
func SpeakerSymbol(_ value: CGFloat) -> String {
switch(value) {
case 0:
return "speaker"
case 0...0.3:
return "speaker.wave.1"
case 0.3...0.8:
return "speaker.wave.2"
case 0.8...1:
return "speaker.wave.3"
default:
return "speaker.wave.2"
}
}
func BrightnessSymbol(_ value: CGFloat) -> String {
switch(value) {
case 0...0.6:
return "sun.min"
case 0.6...1:
return "sun.max"
default:
return "sun.min"
}
}
func Type2Name(_ type: SneakContentType) -> String {
switch(type) {
case .volume:
return "Volume"
case .brightness:
return "Brightness"
case .backlight:
return "Backlight"
case .mic:
return "Mic"
default:
return ""
}
}
}
#Preview {
InlineHUD(type: .constant(.brightness), value: .constant(0.4), icon: .constant(""), hoverAnimation: .constant(false), gestureProgress: .constant(0))
.padding(.horizontal, 8)
.background(Color.black)
.padding()
.environmentObject(DynamicIslandViewModel())
}

View file

@ -0,0 +1,43 @@
//
// LiveActivityModifier.swift
// boringNotch
//
// Created by Richard Kunkli on 12/08/2024.
//
import SwiftUI
enum ActivityType {
case mediaPlayback
case charging
case download
}
struct LiveActivityModifier<Left: View, Right: View>: ViewModifier {
let `for`: ActivityType
let leftContent: () -> Left
let rightContent: () -> Right
func body(content: Content) -> some View {
content
.overlay(
HStack {
leftContent()
Spacer()
//.frame(minWidth: vm.closedNotchSize.width)
rightContent()
}
.padding()
)
}
}
extension View {
func liveActivity<Left: View, Right: View>(
for activityId: ActivityType,
@ViewBuilder left: @escaping () -> Left,
@ViewBuilder right: @escaping () -> Right
) -> some View {
self.modifier(LiveActivityModifier(for: activityId, leftContent: left, rightContent: right))
}
}

View file

@ -0,0 +1,93 @@
//
// MarqueeTextView.swift
// boringNotch
//
// Created by Richard Kunkli on 08/08/2024.
//
import SwiftUI
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct MeasureSizeModifier: ViewModifier {
func body(content: Content) -> some View {
content.background(GeometryReader { geometry in
Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
})
}
}
struct MarqueeText: View {
@Binding var text: String
let font: Font
let nsFont: NSFont.TextStyle
let textColor: Color
let backgroundColor: Color
let minDuration: Double
let frameWidth: CGFloat
@State private var animate = false
@State private var textSize: CGSize = .zero
@State private var offset: CGFloat = 0
init(_ text: Binding<String>, font: Font = .body, nsFont: NSFont.TextStyle = .body, textColor: Color = .primary, backgroundColor: Color = .clear, minDuration: Double = 3.0, frameWidth: CGFloat = 200) {
_text = text
self.font = font
self.nsFont = nsFont
self.textColor = textColor
self.backgroundColor = backgroundColor
self.minDuration = minDuration
self.frameWidth = frameWidth
}
private var needsScrolling: Bool {
textSize.width > frameWidth
}
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
HStack(spacing: 20) {
Text(text)
Text(text)
.opacity(needsScrolling ? 1 : 0)
}
.id(text)
.font(font)
.foregroundColor(textColor)
.fixedSize(horizontal: true, vertical: false)
.offset(x: self.animate ? offset : 0)
.animation(
self.animate ?
.linear(duration: Double(textSize.width / 30))
.delay(minDuration)
.repeatForever(autoreverses: false) : .none,
value: self.animate
)
.background(backgroundColor)
.modifier(MeasureSizeModifier())
.onPreferenceChange(SizePreferenceKey.self) { size in
self.textSize = CGSize(width: size.width / 2, height: NSFont.preferredFont(forTextStyle: nsFont).pointSize)
self.animate = false
self.offset = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01){
if needsScrolling {
self.animate = true
self.offset = -(textSize.width + 10)
}
}
}
}
.frame(width: frameWidth, alignment: .leading)
.clipped()
}
.frame(height: textSize.height * 1.3)
}
}

View file

@ -0,0 +1,144 @@
//
// SystemEventIndicatorModifier.swift
// boringNotch
//
// Created by Richard Kunkli on 12/08/2024.
//
import SwiftUI
import Defaults
struct SystemEventIndicatorModifier: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@Binding var eventType: SneakContentType
@Binding var value: CGFloat {
didSet {
DispatchQueue.main.async {
self.sendEventBack(value)
self.vm.objectWillChange.send()
}
}
}
@Binding var icon: String
let showSlider: Bool = false
var sendEventBack: (CGFloat) -> Void
var body: some View {
HStack(spacing: 14) {
switch (eventType) {
case .volume:
if icon.isEmpty {
Image(systemName: SpeakerSymbol(value))
.contentTransition(.interpolate)
.symbolVariant(value > 0 ? .none : .slash)
.frame(width: 20, height: 15, alignment: .leading)
} else {
Image(systemName: icon)
.contentTransition(.interpolate)
.opacity(value.isZero ? 0.6 : 1)
.scaleEffect(value.isZero ? 0.85 : 1)
.frame(width: 20, height: 15, alignment: .leading)
}
case .brightness:
Image(systemName: "sun.max.fill")
.contentTransition(.symbolEffect)
.frame(width: 20, height: 15)
.foregroundStyle(.white)
case .backlight:
Image(systemName: "keyboard")
.contentTransition(.symbolEffect)
.frame(width: 20, height: 15)
.foregroundStyle(.white)
case .mic:
Image(systemName: "mic")
.symbolVariant(value > 0 ? .none : .slash)
.contentTransition(.interpolate)
.frame(width: 20, height: 15)
.foregroundStyle(.white)
default:
EmptyView()
}
if (eventType != .mic) {
DraggableProgressBar(value: $value)
} else {
Text("Mic \(value > 0 ? "unmuted" : "muted")")
.foregroundStyle(.gray)
.lineLimit(1)
.allowsTightening(true)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.symbolVariant(.fill)
.imageScale(.large)
}
func SpeakerSymbol(_ value: CGFloat) -> String {
switch(value) {
case 0:
return "speaker.slash"
case 0...0.3:
return "speaker.wave.1"
case 0.3...0.8:
return "speaker.wave.2"
case 0.8...1:
return "speaker.wave.3"
default:
return "speaker.wave.2"
}
}
}
struct DraggableProgressBar: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@Binding var value: CGFloat
@State private var isDragging = false
@State private var dragOffset: CGFloat = 0
var body: some View {
VStack {
GeometryReader { geo in
ZStack(alignment: .leading) {
Capsule()
.fill(.tertiary)
Group {
if Defaults[.enableGradient] {
Capsule()
.fill(LinearGradient(colors: Defaults[.systemEventIndicatorUseAccent] ? [Defaults[.accentColor], Defaults[.accentColor].ensureMinimumBrightness(factor: 0.2)] : [.white, .white.opacity(0.2)], startPoint: .trailing, endPoint: .leading))
.frame(width: max(0, min(geo.size.width * value, geo.size.width)))
.shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3)
} else {
Capsule()
.fill(Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor] : .white)
.frame(width: max(0, min(geo.size.width * value, geo.size.width)))
.shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3)
}
}
.opacity(value.isZero ? 0 : 1)
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
withAnimation(.smooth(duration: 0.3)) {
isDragging = true
updateValue(gesture: gesture, in: geo)
}
}
.onEnded { _ in
withAnimation(.smooth(duration: 0.3)) {
isDragging = false
}
}
)
}
.frame(height: Defaults[.inlineHUD] ? isDragging ? 8 : 5 : isDragging ? 9 : 6)
}
}
private func updateValue(gesture: DragGesture.Value, in geometry: GeometryProxy) {
let dragPosition = gesture.location.x
let newValue = dragPosition / geometry.size.width
value = max(0, min(newValue, 1))
}
}

View file

@ -0,0 +1,33 @@
//
// LottieAnimationView.swift
// boringNotch
//
// Created by Richard Kunkli on 2024. 10. 29..
//
import SwiftUI
import Lottie
import LottieUI
import Defaults
struct LottieAnimationView: View {
let state1 = LUStateData(type: .loadedFrom(URL(string: "https://assets9.lottiefiles.com/packages/lf20_mniampqn.json")!), speed: 1.0, loopMode: .loop)
@Default(.selectedVisualizer) var selectedVisualizer
var body: some View {
if selectedVisualizer == nil {
LottieView(state: state1)
} else {
LottieView(
state: LUStateData(
type: .loadedFrom(selectedVisualizer!.url),
speed: selectedVisualizer!.speed,
loopMode: .loop
)
)
}
}
}
#Preview {
LottieAnimationView()
}

View file

@ -0,0 +1,115 @@
//
// MusicVisualizer.swift
// boringNotch
//
// Created by Harsh Vardhan Goswami on 02/08/24.
//
import AppKit
import Cocoa
import SwiftUI
class AudioSpectrum: NSView {
private var barLayers: [CAShapeLayer] = []
private var isPlaying: Bool = true
private var animationTimer: Timer?
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
wantsLayer = true
setupBars()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
wantsLayer = true
setupBars()
}
private func setupBars() {
let barWidth: CGFloat = 2
let barCount = 4
let spacing: CGFloat = barWidth
let totalWidth = CGFloat(barCount) * (barWidth + spacing)
let totalHeight: CGFloat = 14
frame.size = CGSize(width: totalWidth, height: totalHeight)
for i in 0 ..< barCount {
let xPosition = CGFloat(i) * (barWidth + spacing)
let barLayer = CAShapeLayer()
barLayer.frame = CGRect(x: xPosition, y: 0, width: barWidth, height: totalHeight)
barLayer.position = CGPoint(x: xPosition + barWidth / 2, y: totalHeight / 2)
barLayer.fillColor = NSColor.white.cgColor
let path = NSBezierPath(roundedRect: CGRect(x: 0, y: 0, width: barWidth, height: totalHeight),
xRadius: barWidth / 2,
yRadius: barWidth / 2)
barLayer.path = path.cgPath
barLayers.append(barLayer)
layer?.addSublayer(barLayer)
}
}
private func startAnimating() {
guard animationTimer == nil else { return }
animationTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { [weak self] _ in
self?.updateBars()
}
}
private func stopAnimating() {
animationTimer?.invalidate()
animationTimer = nil
resetBars()
}
private func updateBars() {
for barLayer in barLayers {
let animation = CABasicAnimation(keyPath: "transform.scale.y")
animation.fromValue = barLayer.presentation()?.value(forKeyPath: "transform.scale.y") ?? 0.35
animation.toValue = CGFloat.random(in: 0.35 ... 1.0)
animation.duration = 0.3
animation.autoreverses = true
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
barLayer.add(animation, forKey: "scaleY")
}
}
private func resetBars() {
for barLayer in barLayers {
barLayer.removeAllAnimations()
barLayer.transform = CATransform3DMakeScale(1, 0.35, 1)
}
}
func setPlaying(_ playing: Bool) {
isPlaying = playing
if isPlaying {
startAnimating()
} else {
stopAnimating()
}
}
}
struct AudioSpectrumView: NSViewRepresentable {
@Binding var isPlaying: Bool
func makeNSView(context: Context) -> AudioSpectrum {
let spectrum = AudioSpectrum()
spectrum.setPlaying(isPlaying)
return spectrum
}
func updateNSView(_ nsView: AudioSpectrum, context: Context) {
nsView.setPlaying(isPlaying)
}
}
#Preview {
AudioSpectrumView(isPlaying: .constant(true))
.frame(width: 16, height: 20)
.padding()
}

View 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())
}

View 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())
}

View 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
}
}

View 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())
}

View 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)
}

View 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)
}
}
}
}

View file

@ -0,0 +1,80 @@
//
// OnboardingSettings.swift
// boringNotch
//
// Created by Richard Kunkli on 2024. 09. 26..
//
import SwiftUI
struct ActivationWindow: View {
@State private var email: String = ""
@State private var key: String = ""
var body: some View {
VStack {
Image("logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 80)
.padding(.top, 30)
.padding(.bottom, 10)
Text("Activate your license")
.font(.largeTitle.bold())
.fontDesign(.rounded)
Text("Transform your notch truly yours")
.foregroundStyle(.secondary)
.font(.title2)
.padding(.bottom, 20)
Group {
TextField("Email address", text: $email)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
TextField("License key", text: $key)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
}
.textFieldStyle(PlainTextFieldStyle())
.scrollContentBackground(.hidden)
.toggleStyle(.switch)
Spacer()
VStack(alignment: .center, spacing: 14) {
HStack(alignment: .center) {
HStack {
Button {} label: {
Text("Cancel")
.padding(.horizontal, 18)
}
.buttonStyle(AccessoryBarButtonStyle())
}
.frame(maxWidth: .infinity, alignment: .leading)
Image("dynamicisland")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 18)
.offset(y: 4)
.blendMode(.overlay)
HStack {
Button {} label: {
Text("Activate")
.padding(.horizontal, 18)
}
.buttonStyle(BorderedProminentButtonStyle())
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.controlSize(.extraLarge)
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.ignoresSafeArea()
.background {
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
.ignoresSafeArea()
}
.frame(width: 350, height: 350)
}
}

View file

@ -0,0 +1,105 @@
//
// ProOnboarding.swift
// boringNotch
//
// Created by Richard Kunkli on 2024. 09. 26..
//
import SwiftUI
import SwiftUIIntrospect
struct ProOnboard: View {
var body: some View {
ZStack(alignment: .top) {
ZStack {
Image("spotlight")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.bottom)
.blur(radius: 3)
.offset(y: -5)
.background(SparkleView().opacity(0.6))
VStack(spacing: 8) {
Image("logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
.padding(.bottom, 8)
Text("TheDynamicIsland")
.font(.system(.largeTitle, design: .serif))
Text("Welcome")
.font(.title)
.foregroundStyle(.secondary)
.padding(.bottom, 30)
if false {
Text("PRO")
.font(.system(size: 18, design: .rounded))
.fontWeight(.bold)
.foregroundStyle(.white)
.padding(.horizontal, 12)
.padding(.vertical, 3)
.background(
Capsule()
.fill(LinearGradient(colors: [.white.opacity(0.7), .white.opacity(0.3)], startPoint: .topLeading, endPoint: .bottomTrailing))
.strokeBorder(LinearGradient(stops: [.init(color: .white.opacity(0.7), location: 0.3), .init(color: .clear, location: 0.6)], startPoint: .topLeading, endPoint: .bottomTrailing))
.blendMode(.overlay)
)
.padding(.bottom, 30)
}
Button {
if let url = URL(string: "https://buymeacoffee.com/jfxh67wvfxq") {
NSWorkspace.shared.open(url)
}
} label: {
Label("Buy us a coffee", systemImage: "cup.and.saucer.fill")
.font(.headline)
.foregroundColor(Color.black)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.yellow)
.cornerRadius(8)
}
.buttonStyle(PlainButtonStyle())
.padding(.bottom, 20)
Button {
NSApp.keyWindow?.close()
} label: {
Text("Get started")
.padding(.horizontal, 20)
.padding(.vertical, 6)
}
.buttonStyle(BorderedProminentButtonStyle())
}
.padding(.top)
}
Image("dynamicisland")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 22)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.padding()
.padding(.bottom, 36)
.blendMode(.overlay)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.ignoresSafeArea()
.background {
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
.ignoresSafeArea()
}
.onAppear {
NSApp.mainWindow?.standardWindowButton(.zoomButton)?.isHidden = true
NSApp.mainWindow?.standardWindowButton(.closeButton)?.isHidden = true
NSApp.mainWindow?.standardWindowButton(.miniaturizeButton)?.isHidden = true
NSApp.mainWindow?.styleMask.remove(.resizable)
}
.frame(width: 350, height: 500)
.introspect(.window, on: .macOS(.v14, .v15)) { window in
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.center()
}
}
}

View file

@ -0,0 +1,76 @@
//
// SparkleView.swift
// boringNotch
//
// Created by Richard Kunkli on 2024. 09. 26..
//
import SwiftUI
import AppKit
class SparkleNSView: NSView {
private var emitterLayer: CAEmitterLayer?
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.wantsLayer = true
setupEmitterLayer()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupEmitterLayer() {
let emitterLayer = CAEmitterLayer()
emitterLayer.emitterShape = .rectangle
emitterLayer.emitterMode = .surface
emitterLayer.renderMode = .oldestFirst
let cell = CAEmitterCell()
cell.contents = NSImage(named: "sparkle")?.cgImage(forProposedRect: nil, context: nil, hints: nil)
cell.birthRate = 50
cell.lifetime = 5
cell.velocity = 10
cell.velocityRange = 5
cell.emissionRange = .pi * 2
cell.scale = 0.2
cell.scaleRange = 0.1
cell.alphaSpeed = -0.5
cell.yAcceleration = 10 // Add a slight downward motion
emitterLayer.emitterCells = [cell]
self.layer?.addSublayer(emitterLayer)
self.emitterLayer = emitterLayer
updateEmitterForCurrentBounds()
}
private func updateEmitterForCurrentBounds() {
guard let emitterLayer = self.emitterLayer else { return }
emitterLayer.frame = self.bounds
emitterLayer.emitterSize = self.bounds.size
emitterLayer.emitterPosition = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
// Adjust birth rate based on view size
let area = bounds.width * bounds.height
let baseBirthRate: Float = 50
let adjustedBirthRate = 20 // Assuming 200x200 as base size
emitterLayer.emitterCells?.first?.birthRate = Float(adjustedBirthRate)
}
override func setFrameSize(_ newSize: NSSize) {
super.setFrameSize(newSize)
updateEmitterForCurrentBounds()
}
}
struct SparkleView: NSViewRepresentable {
func makeNSView(context: Context) -> SparkleNSView {
return SparkleNSView()
}
func updateNSView(_ nsView: SparkleNSView, context: Context) {}
}

View file

@ -0,0 +1,63 @@
//
// ProgressIndicator.swift
// boringNotch
//
// Created by Harsh Vardhan Goswami on 11/08/24.
//
import Foundation
import SwiftUI
struct CircularProgressView: View {
let progress: Double
let color: Color
var body: some View {
ZStack {
Circle()
.stroke(
Color.white.opacity(0.2),
lineWidth: 6
)
Circle()
.trim(from: 0, to: progress)
.stroke(
color,
// 1
style: StrokeStyle(
lineWidth: 6,
lineCap: .round
)
)
.rotationEffect(.degrees(-90))
}
}
}
enum ProgressIndicatorType {
case circle
case text
}
// based on type .circle or .text
struct ProgressIndicator: View {
var type: ProgressIndicatorType
var progress: Double
var color: Color
var body: some View {
switch type {
case .circle:
CircularProgressView(progress: progress, color: color).frame(
width: 20, height: 20)
case .text:
Text("\(Int(progress * 100))%")
}
}
}
#Preview {
ProgressIndicator(type: .circle, progress: 0.8, color: Color.blue).padding()
.frame(width: 200, height: 200)
}

View file

@ -0,0 +1,54 @@
//
// EditPanelView.swift
// boringNotch
//
// Created by Richard Kunkli on 12/08/2024.
//
import SwiftUI
struct EditPanelView: View {
@State var wallpaperPath: URL?
var body: some View {
VStack {
HStack {
Text("Edit layout")
.font(.system(.largeTitle, design: .rounded))
.foregroundColor(.white.opacity(0.5))
Spacer()
Button {
exit(0)
} label: {
Label("Close", systemImage: "xmark")
}
.controlSize(.extraLarge)
.buttonStyle(AccessoryBarButtonStyle())
}
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
}
#Preview {
EditPanelView()
}
struct VisualEffectView: NSViewRepresentable {
let material: NSVisualEffectView.Material
let blendingMode: NSVisualEffectView.BlendingMode
func makeNSView(context _: Context) -> NSVisualEffectView {
let visualEffectView = NSVisualEffectView()
visualEffectView.material = material
visualEffectView.blendingMode = blendingMode
visualEffectView.state = NSVisualEffectView.State.active
visualEffectView.isEmphasized = true
return visualEffectView
}
func updateNSView(_ visualEffectView: NSVisualEffectView, context _: Context) {
visualEffectView.material = material
visualEffectView.blendingMode = blendingMode
}
}

View file

@ -0,0 +1,28 @@
//
// ListItemPopover.swift
// boringNotch
//
// Created by Richard Kunkli on 15/09/2024.
//
import SwiftUI
struct ListItemPopover<Content: View>: View {
let content: () -> Content
@State private var isPresented: Bool = false
var body: some View {
Button {
isPresented.toggle()
} label: {
Image(systemName: "info.circle")
.foregroundStyle(.secondary)
}
.controlSize(.regular)
.popover(isPresented: $isPresented, attachmentAnchor: .rect(.bounds), arrowEdge: .trailing, content: {
content()
.padding()
})
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
//
// SoftwareUpdater.swift
// boringNotch
//
// Created by Richard Kunkli on 09/08/2024.
//
import SwiftUI
import Sparkle
final class CheckForUpdatesViewModel: ObservableObject {
@Published var canCheckForUpdates = false
init(updater: SPUUpdater) {
updater.publisher(for: \.canCheckForUpdates)
.assign(to: &$canCheckForUpdates)
}
}
struct CheckForUpdatesView: View {
@ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel
private let updater: SPUUpdater
init(updater: SPUUpdater) {
self.updater = updater
// Create our view model for our CheckForUpdatesView
self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater)
}
var body: some View {
Button("Check for Updates…", action: updater.checkForUpdates)
.disabled(!checkForUpdatesViewModel.canCheckForUpdates)
}
}
struct UpdaterSettingsView: View {
private let updater: SPUUpdater
@State private var automaticallyChecksForUpdates: Bool
@State private var automaticallyDownloadsUpdates: Bool
init(updater: SPUUpdater) {
self.updater = updater
self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates
self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates
}
var body: some View {
Section {
Toggle("Automatically check for updates", isOn: $automaticallyChecksForUpdates)
.onChange(of: automaticallyChecksForUpdates) { _, newValue in
updater.automaticallyChecksForUpdates = newValue
}
Toggle("Automatically download updates", isOn: $automaticallyDownloadsUpdates)
.disabled(!automaticallyChecksForUpdates)
.onChange(of: automaticallyDownloadsUpdates) { _, newValue in
updater.automaticallyDownloadsUpdates = newValue
}
} header: {
HStack {
Text("Software updates")
}
}
}
}

View file

@ -0,0 +1,40 @@
//
// AirDrop.swift
// NotchDrop
//
// Created by 秋星桥 on 2024/7/7.
//
import Cocoa
class AirDrop: NSObject, NSSharingServiceDelegate {
let files: [URL]
init(files: [URL]) {
self.files = files
super.init()
}
func begin() {
do {
try sendEx(files)
} catch {
NSAlert.popError(error)
}
}
private func sendEx(_ files: [URL]) throws {
guard let service = NSSharingService(named: .sendViaAirDrop) else {
throw NSError(domain: "AirDrop", code: 1, userInfo: [
NSLocalizedDescriptionKey: NSLocalizedString("AirDrop service not available", comment: ""),
])
}
guard service.canPerform(withItems: files) else {
throw NSError(domain: "AirDrop", code: 2, userInfo: [
NSLocalizedDescriptionKey: NSLocalizedString("AirDrop service not available", comment: ""),
])
}
service.delegate = self
service.perform(withItems: files)
}
}

View file

@ -0,0 +1,70 @@
//
// AirDrop+View.swift
// NotchDrop
//
// Created by 秋星桥 on 2024/7/8.
//
import SwiftUI
import UniformTypeIdentifiers
struct AirDropView: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@State var trigger: UUID = .init()
@State var targeting = false
var body: some View {
dropArea
.onDrop(of: [.data], isTargeted: $vm.dropZoneTargeting) { providers in
trigger = .init()
vm.dropEvent = true
DispatchQueue.global().async { beginDrop(providers) }
return true
}
}
var dropArea: some View {
Rectangle()
.fill(.white.opacity(0.1))
.opacity(0.5)
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay { dropLabel }
.aspectRatio(1, contentMode: .fit)
.contentShape(Rectangle())
}
var dropLabel: some View {
VStack(spacing: 8) {
Image(systemName: "airplayaudio")
Text("AirDrop")
}
.foregroundStyle(.gray)
.font(.system(.headline, design: .rounded))
.contentShape(Rectangle())
.onTapGesture {
trigger = .init()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let picker = NSOpenPanel()
picker.allowsMultipleSelection = true
picker.canChooseDirectories = true
picker.canChooseFiles = true
picker.begin { response in
if response == .OK {
let drop = AirDrop(files: picker.urls)
drop.begin()
}
}
}
}
}
func beginDrop(_ providers: [NSItemProvider]) {
assert(!Thread.isMainThread)
guard let urls = providers.interfaceConvert() else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let drop = AirDrop(files: urls)
drop.begin()
}
}
}

View file

@ -0,0 +1,56 @@
//
// DragDropView.swift
// boringNotch
//
// Created by Richard Kunkli on 2024. 10. 19..
//
import SwiftUI
import AppKit
class DragDropView: NSView {
var onDragEntered: () -> Void = {}
var onDragExited: () -> Void = {}
var onDrop: () -> Void = {}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
registerForDraggedTypes([.fileURL])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
onDragEntered()
return .copy
}
override func draggingExited(_ sender: NSDraggingInfo?) {
onDragExited()
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
onDrop()
return true
}
}
struct DragDropViewRepresentable: NSViewRepresentable {
@Binding var isTargeted: Bool
var onDrop: () -> Void
func makeNSView(context: Context) -> DragDropView {
let view = DragDropView()
view.onDragEntered = { isTargeted = true }
view.onDragExited = { isTargeted = false }
view.onDrop = onDrop
view.autoresizingMask = [.width, .height]
return view
}
func updateNSView(_ nsView: DragDropView, context: Context) {}
}

View file

@ -0,0 +1,58 @@
//
// TrayDrop+DropItem.swift
// TrayDrop
//
// Created by 秋星桥 on 2024/7/8.
//
import Cocoa
import Foundation
import QuickLook
extension TrayDrop {
struct DropItem: Identifiable, Codable, Equatable, Hashable {
let id: UUID
let fileName: String
let size: Int
let copiedDate: Date
let workspacePreviewImageData: Data
init(url: URL) throws {
assert(!Thread.isMainThread)
id = UUID()
fileName = url.lastPathComponent
size = (try? url.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? 0
copiedDate = Date()
workspacePreviewImageData = url.snapshotPreview().pngRepresentation
try FileManager.default.createDirectory(
at: storageURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try FileManager.default.copyItem(at: url, to: storageURL)
}
}
}
extension TrayDrop.DropItem {
static let mainDir = "CopiedItems"
var storageURL: URL {
documentsDirectory
.appendingPathComponent(Self.mainDir)
.appendingPathComponent(id.uuidString)
.appendingPathComponent(fileName)
}
var workspacePreviewImage: NSImage {
.init(data: workspacePreviewImageData) ?? .init()
}
var shouldClean: Bool { // TODO: In the future clean if old
return false
}
}

View file

@ -0,0 +1,79 @@
//
// TrayDrop+DropItemView.swift
// NotchDrop
//
// Created by 秋星桥 on 2024/7/8.
//
import Foundation
import SwiftUI
import UniformTypeIdentifiers
struct DropItemView: View {
let item: TrayDrop.DropItem
@EnvironmentObject var vm: DynamicIslandViewModel
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@StateObject var tvm = TrayDrop.shared
@State var hover = false
var body: some View {
ZStack(alignment: .topTrailing) {
VStack {
RoundedRectangle(cornerRadius: 6)
.fill(.clear)
.background {
Image(nsImage: item.workspacePreviewImage)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
Text(item.fileName)
.multilineTextAlignment(.center)
.font(.footnote)
.foregroundStyle(hover ? .white : .gray)
.lineLimit(2)
.allowsTightening(true)
}
.contentShape(Rectangle())
.onDrag {
handleOnDrag(for: item)
}
.frame(width: 64, height: 64)
if hover {
Circle()
.fill(.white)
.overlay(Image(systemName: "xmark").foregroundStyle(.black).font(.system(size: 7)).fontWeight(.semibold))
.frame(width: spacing, height: spacing)
.opacity(hover ? 1 : 0) // TODO: Use option key pressed to show delete
.scaleEffect(coordinator.optionKeyPressed ? 1 : 0.5)
.transition(.blurReplace.combined(with: .scale))
.offset(x: spacing / 2, y: -spacing / 2)
.onTapGesture { tvm.delete(item.id) }
.shadow(color: .black, radius: 3)
}
}
.onHover { hovering in
withAnimation(.smooth) {
if hovering {
hover.toggle()
} else {
hover.toggle()
}
}
}
}
private func handleOnDrag(for item: TrayDrop.DropItem) -> NSItemProvider {
guard let itemProvider = NSItemProvider(contentsOf: item.storageURL) else {
return NSItemProvider()
}
let nameWithoutExtension = (item.fileName as NSString).deletingPathExtension
itemProvider.suggestedName = nameWithoutExtension
return itemProvider
}
}

View file

@ -0,0 +1,61 @@
//
// Ext+FileProvider.swift
// NotchDrop
//
// Created by 秋星桥 on 2024/7/8.
//
import Cocoa
import Foundation
import UniformTypeIdentifiers
extension NSItemProvider {
private func duplicateToOurStorage(_ url: URL?) throws -> URL? {
guard let url else { return nil }
let temp = temporaryDirectory
.appendingPathComponent("TemporaryDrop")
.appendingPathComponent(UUID().uuidString)
.appendingPathComponent(url.lastPathComponent)
try? FileManager.default.createDirectory(
at: temp.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try FileManager.default.copyItem(at: url, to: temp)
return temp
}
func convertToFilePathThatIsWhatWeThinkItWillWorkWithNotchDrop() -> URL? {
var url: URL?
let sem = DispatchSemaphore(value: 0)
_ = loadObject(ofClass: URL.self) { item, _ in
url = try? self.duplicateToOurStorage(item)
sem.signal()
}
sem.wait()
if url == nil {
loadInPlaceFileRepresentation(
forTypeIdentifier: UTType.data.identifier
) { input, _, _ in
defer { sem.signal() }
url = try? self.duplicateToOurStorage(input)
}
sem.wait()
}
return url
}
}
extension [NSItemProvider] {
func interfaceConvert() -> [URL]? {
let urls = compactMap { provider -> URL? in
provider.convertToFilePathThatIsWhatWeThinkItWillWorkWithNotchDrop()
}
guard urls.count == count else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
NSAlert.popError(NSLocalizedString("One or more files failed to load", comment: ""))
}
return nil
}
return urls
}
}

View file

@ -0,0 +1,36 @@
//
// Ext+NSAlert.swift
// NotchDrop
//
// Created by 秋星桥 on 2024/7/9.
//
import Cocoa
extension NSAlert {
static func popError(_ error: String) {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Error", comment: "")
alert.alertStyle = .critical
alert.informativeText = error
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.runModal()
}
static func popRestart(_ error: String, completion: @escaping () -> Void) {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Need Restart", comment: "")
alert.alertStyle = .critical
alert.informativeText = error
alert.addButton(withTitle: NSLocalizedString("Exit", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Later", comment: ""))
let response = alert.runModal()
if response == .alertFirstButtonReturn {
completion()
}
}
static func popError(_ error: Error) {
popError(error.localizedDescription)
}
}

View file

@ -0,0 +1,19 @@
//
// Ext+NSImage.swift
// NotchDrop
//
// Created by 秋星桥 on 2024/7/8.
//
import Cocoa
extension NSImage {
var pngRepresentation: Data {
guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else {
return .init()
}
let imageRep = NSBitmapImageRep(cgImage: cgImage)
imageRep.size = size
return imageRep.representation(using: .png, properties: [:]) ?? .init()
}
}

View file

@ -0,0 +1,24 @@
//
// Ext+URL.swift
// NotchDrop
//
// Created by 秋星桥 on 2024/7/8.
//
import Cocoa
import Foundation
import QuickLook
extension URL {
func snapshotPreview() -> NSImage {
if let preview = QLThumbnailImageCreate(
kCFAllocatorDefault,
self as CFURL,
CGSize(width: 128, height: 128),
nil
)?.takeRetainedValue() {
return NSImage(cgImage: preview, size: .zero)
}
return NSWorkspace.shared.icon(forFile: path)
}
}

View file

@ -0,0 +1,77 @@
import Cocoa
import Combine
import Foundation
import OrderedCollections
class TrayDrop: ObservableObject {
static let shared = TrayDrop()
@Published var items: OrderedSet<DropItem>
var isEmpty: Bool { items.isEmpty }
@Published var isLoading: Int = 0
init(items: OrderedSet<DropItem> = .init(), isLoading: Int = 0) {
self.items = items
self.isLoading = isLoading
}
func load(_ providers: [NSItemProvider]) {
assert(!Thread.isMainThread)
DispatchQueue.main.asyncAndWait { isLoading += 1 }
guard let urls = providers.interfaceConvert() else {
DispatchQueue.main.asyncAndWait { isLoading -= 1 }
print("Faield to load items")
return
}
let dropItems = urls.map { url in
try? DropItem(url: url)
}.compactMap { $0 }
DispatchQueue.main.async {
dropItems.forEach { self.items.updateOrInsert($0, at: 0) }
self.isLoading -= 1
}
print("DONE")
}
func cleanExpiredFiles() {
var inEdit = items
let shouldCleanItems = items.filter(\.shouldClean)
for item in shouldCleanItems {
inEdit.remove(item)
}
items = inEdit
}
func delete(_ item: DropItem.ID) {
guard let item = items.first(where: { $0.id == item }) else { return }
delete(item: item)
}
private func delete(item: DropItem) {
var inEdit = items
var url = item.storageURL
try? FileManager.default.removeItem(at: url)
do {
// loops up to the main directory
url = url.deletingLastPathComponent()
while url.lastPathComponent != DropItem.mainDir, url != documentsDirectory {
let contents = try FileManager.default.contentsOfDirectory(atPath: url.path)
guard contents.isEmpty else { break }
try FileManager.default.removeItem(at: url)
url = url.deletingLastPathComponent()
}
} catch {}
inEdit.remove(item)
items = inEdit
}
func removeAll() {
items.forEach { delete(item: $0) }
}
}

View file

@ -0,0 +1,30 @@
//
// TabButton.swift
// boringNotch
//
// Created by Hugo Persson on 2024-08-24.
//
import SwiftUI
struct TabButton: View {
let label: String
let icon: String
let selected: Bool
let onClick: () -> Void
var body: some View {
Button(action: onClick) {
Image(systemName: icon)
.padding(.horizontal, 15)
.contentShape(Capsule())
}
.buttonStyle(PlainButtonStyle())
}
}
#Preview {
TabButton(label: "Home", icon: "tray.fill", selected: true) {
print("Tapped")
}
}

View file

@ -0,0 +1,55 @@
//
// TabSelectionView.swift
// boringNotch
//
// Created by Hugo Persson on 2024-08-25.
//
import SwiftUI
struct TabModel: Identifiable {
let id = UUID()
let label: String
let icon: String
let view: NotchViews
}
let tabs = [
TabModel(label: "Home", icon: "house.fill", view: .home),
TabModel(label: "Shelf", icon: "tray.fill", view: .shelf)
]
struct TabSelectionView: View {
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@Namespace var animation
var body: some View {
HStack(spacing: 0) {
ForEach(tabs) { tab in
TabButton(label: tab.label, icon: tab.icon, selected: coordinator.currentView == tab.view) {
withAnimation(.smooth) {
coordinator.currentView = tab.view
}
}
.frame(height: 26)
.foregroundStyle(tab.view == coordinator.currentView ? .white : .gray)
.background {
if tab.view == coordinator.currentView {
Capsule()
.fill(coordinator.currentView == tab.view ? Color(nsColor: .secondarySystemFill) : Color.clear)
.matchedGeometryEffect(id: "capsule", in: animation)
} else {
Capsule()
.fill(coordinator.currentView == tab.view ? Color(nsColor: .secondarySystemFill) : Color.clear)
.matchedGeometryEffect(id: "capsule", in: animation)
.hidden()
}
}
}
}
.clipShape(Capsule())
}
}
#Preview {
DynamicIslandHeader().environmentObject(DynamicIslandViewModel())
}

View file

@ -0,0 +1,92 @@
//
// TestView.swift
// boringNotch
//
// Created by Richard Kunkli on 14/08/2024.
//
import SwiftUI
struct FluidSlider: View {
private let color: Color = Color.white
@State private var offset: CGFloat = 0
var rectSize = CGSize(width: 300, height: 50)
var rectSize2 = CGSize(width: 200, height: 18)
var circleSize: CGFloat = 35
@GestureState var isDragging: Bool = false
@State var previousOffset: CGFloat = 0
@State private var isBeating: Bool = false
var body: some View {
HStack {
slider
.frame(width: rectSize2.width, height: circleSize)
}
.padding()
.background(.black)
}
private var slider: some View {
ZStack {
Canvas { context, size in
context.addFilter(.alphaThreshold(min: 0.5, max: 1, color: color))
context.addFilter(.blur(radius: 10))
context.drawLayer { ctx in
if let rectangle = ctx.resolveSymbol(id: "Capsule") {
ctx.draw(rectangle, at: CGPoint(x: size.width/2, y: size.height/2))
}
if let circle = ctx.resolveSymbol(id: "Circle") {
ctx.draw(circle, at: CGPoint(x: size.width/2 - rectSize2.width/2 + circleSize/2, y: size.height/2))
}
}
} symbols: {
Capsule()
.frame(width: rectSize2.width, height: rectSize2.height, alignment: .center)
.tag("Capsule")
Circle()
.frame(width: circleSize, height: circleSize, alignment: .center)
.offset(x: offset)
.animation(.spring(), value: isDragging)
.tag("Circle")
}
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.updating($isDragging, body: { _, state, _ in
state = true
})
.onChanged({ value in
self.offset = min(max(self.previousOffset + value.translation.width, 0), rectSize2.width - circleSize)
})
.onEnded({ value in
self.previousOffset = self.offset
})
)
Circle()
.fill(Color.black)
.frame(width: circleSize * 0.6)
.overlay {
Image(systemName: "speaker.wave.2.fill")
.imageScale(.small)
}
.offset(x: (-rectSize2.width/2) + (circleSize/2))
.offset(x: offset)
.animation(.spring(), value: isDragging)
.allowsHitTesting(false)
}
}
private var animation: Animation {
.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0.5)
}
private var percentage: Int {
Int((offset) / (rectSize.width - circleSize) * 100)
}
}
#Preview {
FluidSlider()
}

View file

@ -0,0 +1,74 @@
//
// TipStore.swift
// boringNotch
//
// Created by Richard Kunkli on 15/09/2024.
//
import SwiftUI
import TipKit
struct HUDsTip: Tip {
var title: Text {
Text("Enhance your experience with HUDs")
}
var message: Text? {
Text("Unlock advanced features and improve your experience. Upgrade now for more customizations!")
}
var image: Image? {
AppIcon(for: "dynamicisland.DynamicIsland")
}
var actions: [Action] {
Action {
Text("More")
}
}
}
struct CBTip: Tip {
var title: Text {
Text("Boost your productivity with Clipboard Manager")
}
var message: Text? {
Text("Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!")
}
var image: Image? {
AppIcon(for: "dynamicisland.DynamicIsland")
}
var actions: [Action] {
Action {
Text("More")
}
}
}
struct TipsView: View {
var hudTip = HUDsTip()
var cbTip = CBTip()
var body: some View {
VStack {
TipView(hudTip)
TipView(cbTip)
}
.task {
try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
}
}
}
#Preview {
TipsView()
}

View file

@ -0,0 +1,122 @@
//
// WebcamView.swift
// boringNotch
//
// Created by Harsh Vardhan Goswami on 19/08/24.
//
import AVFoundation
import Defaults
import SwiftUI
struct CameraPreviewView: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@ObservedObject var webcamManager: WebcamManager
// Track if authorization request is in progress to avoid multiple requests
@State private var isRequestingAuthorization: Bool = false
var body: some View {
GeometryReader { geometry in
ZStack {
if let previewLayer = webcamManager.previewLayer {
CameraPreviewLayerView(previewLayer: previewLayer)
.scaleEffect(x: -1, y: 1)
.clipShape(RoundedRectangle(cornerRadius: Defaults[.mirrorShape] == .rectangle ? !Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.closed : MusicPlayerImageSizes.cornerRadiusInset.opened : 100))
.frame(width: geometry.size.width, height: geometry.size.width)
.opacity(webcamManager.isSessionRunning ? 1 : 0)
}
if !webcamManager.isSessionRunning {
ZStack {
RoundedRectangle(cornerRadius: Defaults[.mirrorShape] == .rectangle ? !Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.closed : 12 : 100)
.fill(Color(red: 20/255, green: 20/255, blue: 20/255))
.strokeBorder(.white.opacity(0.04), lineWidth: 1)
.frame(width: geometry.size.width, height: geometry.size.width)
VStack(spacing: 8) {
Image(systemName: webcamManager.authorizationStatus == .denied ? "exclamationmark.triangle" : "web.camera")
.foregroundStyle(.gray)
.font(.system(size: geometry.size.width/3.5))
Text(webcamManager.authorizationStatus == .denied ? "Access Denied" : "Mirror")
.font(.caption2)
.foregroundColor(.gray)
}
}
}
}
.onTapGesture {
handleCameraTap()
}
.onDisappear {
webcamManager.stopSession()
}
}
.aspectRatio(1, contentMode: .fit)
}
private func handleCameraTap() {
if isRequestingAuthorization {
return // Prevent multiple authorization requests
}
switch webcamManager.authorizationStatus {
case .authorized:
if webcamManager.isSessionRunning {
webcamManager.stopSession()
} else if webcamManager.cameraAvailable {
webcamManager.startSession()
}
case .denied, .restricted:
DispatchQueue.main.async {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
let alert = NSAlert()
alert.messageText = "Camera Access Required"
alert.informativeText = "Please allow camera access in System Settings to use the mirror feature."
alert.addButton(withTitle: "Open Settings")
alert.addButton(withTitle: "Cancel")
if alert.runModal() == .alertFirstButtonReturn {
if let settingsURL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera") {
NSWorkspace.shared.open(settingsURL)
}
}
NSApp.setActivationPolicy(.accessory)
NSApp.deactivate()
}
case .notDetermined:
isRequestingAuthorization = true
webcamManager.checkAndRequestVideoAuthorization()
// Reset the request flag after a reasonable delay
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
isRequestingAuthorization = false
}
@unknown default:
break
}
}
}
struct CameraPreviewLayerView: NSViewRepresentable {
let previewLayer: AVCaptureVideoPreviewLayer
func makeNSView(context: Context) -> NSView {
let view = NSView(frame: .zero)
previewLayer.frame = view.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer = previewLayer
view.wantsLayer = true
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
CATransaction.begin()
CATransaction.setDisableActions(true)
previewLayer.frame = nsView.bounds
CATransaction.commit()
}
}
#Preview {
CameraPreviewView(webcamManager: WebcamManager())
}

View file

@ -0,0 +1,35 @@
//
// WhatsNewView.swift
// boringNotch
//
// Created by Richard Kunkli on 09/08/2024.
//
import SwiftUI
struct WhatsNewView: View {
@Binding var isPresented: Bool
var body: some View {
VStack(spacing: 20) {
Text("What's New")
.font(.largeTitle)
VStack(alignment: .leading, spacing: 10) {
Text("• New feature 1")
Text("• Improved performance")
Text("• Bug fixes")
}
Button("Got it!") {
isPresented = false
}
}
.frame(width: 300, height: 200)
.padding()
}
}
#Preview {
WhatsNewView(isPresented: .constant(true))
}