mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +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
82
DynamicIsland/components/AnimatedFace.swift
Normal file
82
DynamicIsland/components/AnimatedFace.swift
Normal 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
|
||||
}
|
||||
}
|
||||
39
DynamicIsland/components/BottomRoundedRectangle.swift
Normal file
39
DynamicIsland/components/BottomRoundedRectangle.swift
Normal 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
|
||||
}
|
||||
}
|
||||
315
DynamicIsland/components/Calender/BoringCalendar.swift
Normal file
315
DynamicIsland/components/Calender/BoringCalendar.swift
Normal 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())
|
||||
}
|
||||
109
DynamicIsland/components/DynamicIslandSystemTiles.swift
Normal file
109
DynamicIsland/components/DynamicIslandSystemTiles.swift
Normal 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()
|
||||
}
|
||||
26
DynamicIsland/components/EmptyState.swift
Normal file
26
DynamicIsland/components/EmptyState.swift
Normal 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")
|
||||
}
|
||||
46
DynamicIsland/components/HoverButton.swift
Normal file
46
DynamicIsland/components/HoverButton.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
DynamicIsland/components/Live activities/DownloadView.swift
Normal file
52
DynamicIsland/components/Live activities/DownloadView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
147
DynamicIsland/components/Live activities/InlineHUD.swift
Normal file
147
DynamicIsland/components/Live activities/InlineHUD.swift
Normal 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())
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
33
DynamicIsland/components/Music/LottieAnimationView.swift
Normal file
33
DynamicIsland/components/Music/LottieAnimationView.swift
Normal 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()
|
||||
}
|
||||
115
DynamicIsland/components/Music/MusicVisualizer.swift
Normal file
115
DynamicIsland/components/Music/MusicVisualizer.swift
Normal 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()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
80
DynamicIsland/components/Onboarding/ActivationView.swift
Normal file
80
DynamicIsland/components/Onboarding/ActivationView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
105
DynamicIsland/components/Onboarding/ProOnboarding.swift
Normal file
105
DynamicIsland/components/Onboarding/ProOnboarding.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
76
DynamicIsland/components/Onboarding/SparkleView.swift
Normal file
76
DynamicIsland/components/Onboarding/SparkleView.swift
Normal 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) {}
|
||||
}
|
||||
63
DynamicIsland/components/ProgressIndicator.swift
Normal file
63
DynamicIsland/components/ProgressIndicator.swift
Normal 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)
|
||||
}
|
||||
54
DynamicIsland/components/Settings/EditPanelView.swift
Normal file
54
DynamicIsland/components/Settings/EditPanelView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
28
DynamicIsland/components/Settings/ListItemPopover.swift
Normal file
28
DynamicIsland/components/Settings/ListItemPopover.swift
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
1172
DynamicIsland/components/Settings/SettingsView.swift
Normal file
1172
DynamicIsland/components/Settings/SettingsView.swift
Normal file
File diff suppressed because it is too large
Load diff
67
DynamicIsland/components/Settings/SoftwareUpdater.swift
Normal file
67
DynamicIsland/components/Settings/SoftwareUpdater.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
DynamicIsland/components/Shelf/AirDrop.swift
Normal file
40
DynamicIsland/components/Shelf/AirDrop.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
70
DynamicIsland/components/Shelf/AirDropView.swift
Normal file
70
DynamicIsland/components/Shelf/AirDropView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
56
DynamicIsland/components/Shelf/DragDropView.swift
Normal file
56
DynamicIsland/components/Shelf/DragDropView.swift
Normal 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) {}
|
||||
}
|
||||
58
DynamicIsland/components/Shelf/DropItem.swift
Normal file
58
DynamicIsland/components/Shelf/DropItem.swift
Normal 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
|
||||
}
|
||||
}
|
||||
79
DynamicIsland/components/Shelf/DropItemView.swift
Normal file
79
DynamicIsland/components/Shelf/DropItemView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
61
DynamicIsland/components/Shelf/Ext+FileProvider.swift
Normal file
61
DynamicIsland/components/Shelf/Ext+FileProvider.swift
Normal 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
|
||||
}
|
||||
}
|
||||
36
DynamicIsland/components/Shelf/Ext+NSAlert.swift
Normal file
36
DynamicIsland/components/Shelf/Ext+NSAlert.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
19
DynamicIsland/components/Shelf/Ext+NSImage.swift
Normal file
19
DynamicIsland/components/Shelf/Ext+NSImage.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
24
DynamicIsland/components/Shelf/Ext+URL.swift
Normal file
24
DynamicIsland/components/Shelf/Ext+URL.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
77
DynamicIsland/components/Shelf/TrayDrop.swift
Normal file
77
DynamicIsland/components/Shelf/TrayDrop.swift
Normal 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) }
|
||||
}
|
||||
}
|
||||
|
||||
30
DynamicIsland/components/Tabs/TabButton.swift
Normal file
30
DynamicIsland/components/Tabs/TabButton.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
55
DynamicIsland/components/Tabs/TabSelectionView.swift
Normal file
55
DynamicIsland/components/Tabs/TabSelectionView.swift
Normal 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())
|
||||
}
|
||||
92
DynamicIsland/components/TestView.swift
Normal file
92
DynamicIsland/components/TestView.swift
Normal 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()
|
||||
}
|
||||
74
DynamicIsland/components/Tips/TipStore.swift
Normal file
74
DynamicIsland/components/Tips/TipStore.swift
Normal 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()
|
||||
}
|
||||
122
DynamicIsland/components/Webcam/WebcamView.swift
Normal file
122
DynamicIsland/components/Webcam/WebcamView.swift
Normal 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())
|
||||
}
|
||||
35
DynamicIsland/components/WhatsNewView.swift
Normal file
35
DynamicIsland/components/WhatsNewView.swift
Normal 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))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue