mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge pull request #31 from Ebullioscopic/ui/minimalistic
UI/minimalistic
This commit is contained in:
commit
f217c704df
12 changed files with 531 additions and 65 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -73,4 +73,5 @@ gemini-apireference.md
|
||||||
|
|
||||||
ScreenshotApp-main
|
ScreenshotApp-main
|
||||||
stats
|
stats
|
||||||
boring.notch
|
boring.notch
|
||||||
|
referenceimages
|
||||||
|
|
@ -33,6 +33,9 @@ struct ContentView: View {
|
||||||
|
|
||||||
// Dynamic sizing based on view type and graph count with smooth transitions
|
// Dynamic sizing based on view type and graph count with smooth transitions
|
||||||
var dynamicNotchSize: CGSize {
|
var dynamicNotchSize: CGSize {
|
||||||
|
// Use minimalistic or normal size based on settings
|
||||||
|
let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
|
||||||
|
|
||||||
// Always return the target size for the current view to ensure smooth transitions
|
// Always return the target size for the current view to ensure smooth transitions
|
||||||
let targetSize: CGSize
|
let targetSize: CGSize
|
||||||
|
|
||||||
|
|
@ -48,14 +51,14 @@ struct ContentView: View {
|
||||||
if enabledGraphsCount > 3 {
|
if enabledGraphsCount > 3 {
|
||||||
// Expand height for 4+ graphs
|
// Expand height for 4+ graphs
|
||||||
targetSize = CGSize(
|
targetSize = CGSize(
|
||||||
width: openNotchSize.width,
|
width: baseSize.width,
|
||||||
height: openNotchSize.height + 65 // Add extra height for second row
|
height: baseSize.height + 65 // Add extra height for second row
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
targetSize = openNotchSize
|
targetSize = baseSize
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targetSize = openNotchSize
|
targetSize = baseSize
|
||||||
}
|
}
|
||||||
|
|
||||||
return targetSize
|
return targetSize
|
||||||
|
|
@ -85,25 +88,35 @@ struct ContentView: View {
|
||||||
|
|
||||||
@Default(.showNotHumanFace) var showNotHumanFace
|
@Default(.showNotHumanFace) var showNotHumanFace
|
||||||
@Default(.useModernCloseAnimation) var useModernCloseAnimation
|
@Default(.useModernCloseAnimation) var useModernCloseAnimation
|
||||||
|
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||||
|
|
||||||
private let extendedHoverPadding: CGFloat = 30
|
private let extendedHoverPadding: CGFloat = 30
|
||||||
private let zeroHeightHoverPadding: CGFloat = 10
|
private let zeroHeightHoverPadding: CGFloat = 10
|
||||||
|
|
||||||
|
// Use minimalistic corner radius ONLY when opened, keep normal when closed
|
||||||
|
private var activeCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) {
|
||||||
|
if enableMinimalisticUI {
|
||||||
|
// Keep normal closed corner radius, use minimalistic when opened
|
||||||
|
return (opened: minimalisticCornerRadiusInsets.opened, closed: cornerRadiusInsets.closed)
|
||||||
|
}
|
||||||
|
return cornerRadiusInsets
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
let mainLayout = NotchLayout()
|
let mainLayout = NotchLayout()
|
||||||
.frame(alignment: .top)
|
.frame(alignment: .top)
|
||||||
.padding(.horizontal, vm.notchState == .open
|
.padding(.horizontal, vm.notchState == .open
|
||||||
? Defaults[.cornerRadiusScaling] ? (cornerRadiusInsets.opened.top - 5) : (cornerRadiusInsets.opened.bottom - 5)
|
? Defaults[.cornerRadiusScaling] ? (activeCornerRadiusInsets.opened.top - 5) : (activeCornerRadiusInsets.opened.bottom - 5)
|
||||||
: cornerRadiusInsets.closed.bottom
|
: activeCornerRadiusInsets.closed.bottom
|
||||||
)
|
)
|
||||||
.padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0)
|
.padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0)
|
||||||
.background(.black)
|
.background(.black)
|
||||||
.mask {
|
.mask {
|
||||||
((vm.notchState == .open) && Defaults[.cornerRadiusScaling])
|
((vm.notchState == .open) && Defaults[.cornerRadiusScaling])
|
||||||
? NotchShape(topCornerRadius: cornerRadiusInsets.opened.top, bottomCornerRadius: cornerRadiusInsets.opened.bottom)
|
? NotchShape(topCornerRadius: activeCornerRadiusInsets.opened.top, bottomCornerRadius: activeCornerRadiusInsets.opened.bottom)
|
||||||
.drawingGroup()
|
.drawingGroup()
|
||||||
: NotchShape(topCornerRadius: cornerRadiusInsets.closed.top, bottomCornerRadius: cornerRadiusInsets.closed.bottom)
|
: NotchShape(topCornerRadius: activeCornerRadiusInsets.closed.top, bottomCornerRadius: activeCornerRadiusInsets.closed.bottom)
|
||||||
.drawingGroup()
|
.drawingGroup()
|
||||||
}
|
}
|
||||||
.padding(.bottom, vm.notchState == .open && Defaults[.extendHoverArea] ? 0 : (vm.effectiveClosedNotchHeight == 0)
|
.padding(.bottom, vm.notchState == .open && Defaults[.extendHoverArea] ? 0 : (vm.effectiveClosedNotchHeight == 0)
|
||||||
|
|
@ -589,7 +602,7 @@ struct ContentView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var dragDetector: some View {
|
var dragDetector: some View {
|
||||||
if Defaults[.dynamicShelf] {
|
if Defaults[.dynamicShelf] && !Defaults[.enableMinimalisticUI] {
|
||||||
Color.clear
|
Color.clear
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
|
|
||||||
|
|
@ -349,9 +349,27 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func calculateRequiredNotchSize() -> CGSize {
|
private func calculateRequiredNotchSize() -> CGSize {
|
||||||
|
// Check if inline sneak peek is showing and notch is closed
|
||||||
|
let isInlineSneakPeekActive = vm.notchState == .closed &&
|
||||||
|
coordinator.expandingView.show &&
|
||||||
|
(coordinator.expandingView.type == .music || coordinator.expandingView.type == .timer) &&
|
||||||
|
Defaults[.enableSneakPeek] &&
|
||||||
|
Defaults[.sneakPeekStyles] == .inline
|
||||||
|
|
||||||
|
// If inline sneak peek is active, use a wider width to accommodate the expanded content
|
||||||
|
if isInlineSneakPeekActive {
|
||||||
|
// Calculate required width for inline sneak peek:
|
||||||
|
// Album art (~32) + Middle section (380) + Visualizer (~32) + padding = ~450
|
||||||
|
let inlineSneakPeekWidth: CGFloat = 450
|
||||||
|
return CGSize(width: inlineSneakPeekWidth, height: vm.effectiveClosedNotchHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use minimalistic or normal size based on settings
|
||||||
|
let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
|
||||||
|
|
||||||
// Only apply dynamic sizing when on stats tab and stats are enabled
|
// Only apply dynamic sizing when on stats tab and stats are enabled
|
||||||
guard coordinator.currentView == .stats && Defaults[.enableStatsFeature] else {
|
guard coordinator.currentView == .stats && Defaults[.enableStatsFeature] else {
|
||||||
return openNotchSize
|
return baseSize
|
||||||
}
|
}
|
||||||
|
|
||||||
let enabledGraphsCount = [
|
let enabledGraphsCount = [
|
||||||
|
|
@ -363,16 +381,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
].filter { $0 }.count
|
].filter { $0 }.count
|
||||||
|
|
||||||
// Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows
|
// Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows
|
||||||
var requiredHeight = openNotchSize.height
|
var requiredHeight = baseSize.height
|
||||||
|
|
||||||
if enabledGraphsCount >= 4 {
|
if enabledGraphsCount >= 4 {
|
||||||
// Two rows needed - add height for second row plus spacing
|
// Two rows needed - add height for second row plus spacing
|
||||||
let extraHeight: CGFloat = 120 + 12 // Graph height + spacing
|
let extraHeight: CGFloat = 120 + 12 // Graph height + spacing
|
||||||
requiredHeight = openNotchSize.height + extraHeight
|
requiredHeight = baseSize.height + extraHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// Width stays constant - no horizontal expansion
|
// Width stays constant - no horizontal expansion
|
||||||
return CGSize(width: openNotchSize.width, height: requiredHeight)
|
return CGSize(width: baseSize.width, height: requiredHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
|
|
@ -423,6 +441,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
self?.debouncedUpdateWindowSize()
|
self?.debouncedUpdateWindowSize()
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
// Observe minimalistic UI setting changes - trigger window resize
|
||||||
|
Defaults.publisher(.enableMinimalisticUI, options: []).sink { [weak self] change in
|
||||||
|
// Force sneak peek to standard mode when minimalistic UI is enabled
|
||||||
|
if change.newValue == true && Defaults[.sneakPeekStyles] != .standard {
|
||||||
|
Defaults[.sneakPeekStyles] = .standard
|
||||||
|
}
|
||||||
|
self?.debouncedUpdateWindowSize()
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
// Observe screen recording settings changes
|
// Observe screen recording settings changes
|
||||||
Defaults.publisher(.enableScreenRecordingDetection, options: []).sink { _ in
|
Defaults.publisher(.enableScreenRecordingDetection, options: []).sink { _ in
|
||||||
if Defaults[.enableScreenRecordingDetection] {
|
if Defaults[.enableScreenRecordingDetection] {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@
|
||||||
},
|
},
|
||||||
" – %lld" : {
|
" – %lld" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"-%@" : {
|
||||||
|
"comment" : "A label displaying the duration of the current song. The argument is the duration of the song in seconds.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"'Now Playing' was the only option on previous versions and works with all media apps." : {
|
"'Now Playing' was the only option on previous versions and works with all media apps." : {
|
||||||
|
|
||||||
|
|
@ -867,6 +871,9 @@
|
||||||
},
|
},
|
||||||
"min" : {
|
"min" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Minimalistic mode focuses on media controls and system HUDs, hiding all extra features for a clean, focused experience. Automatically enables simpler animations." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Minimum hover duration" : {
|
"Minimum hover duration" : {
|
||||||
|
|
||||||
|
|
@ -1362,6 +1369,10 @@
|
||||||
},
|
},
|
||||||
"Sneak Peek Style" : {
|
"Sneak Peek Style" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Sneak peek style is locked to Standard in minimalistic mode" : {
|
||||||
|
"comment" : "A message displayed when the sneak peek style is locked to standard in minimalistic UI mode.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Software updates" : {
|
"Software updates" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -1510,6 +1521,9 @@
|
||||||
},
|
},
|
||||||
"Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" : {
|
"Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"UI Mode" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Uninstall" : {
|
"Uninstall" : {
|
||||||
|
|
||||||
|
|
|
||||||
83
DynamicIsland/components/MinimalisticMusicView.swift
Normal file
83
DynamicIsland/components/MinimalisticMusicView.swift
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
//
|
||||||
|
// MinimalisticMusicView.swift
|
||||||
|
// DynamicIsland
|
||||||
|
//
|
||||||
|
// Created for minimalistic UI mode
|
||||||
|
// A clean, focused music player for closed notch state
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Defaults
|
||||||
|
|
||||||
|
struct MinimalisticMusicView: View {
|
||||||
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
|
@ObservedObject var musicManager = MusicManager.shared
|
||||||
|
@State private var isHovering: Bool = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
// Left: Album Art
|
||||||
|
albumArtView
|
||||||
|
|
||||||
|
// Middle: Song Title (scrolling if needed)
|
||||||
|
Rectangle()
|
||||||
|
.fill(.black)
|
||||||
|
.overlay(
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
if !musicManager.songTitle.isEmpty {
|
||||||
|
MarqueeText(
|
||||||
|
.constant(musicManager.songTitle),
|
||||||
|
textColor: Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor) : Color.gray,
|
||||||
|
minDuration: 0.4,
|
||||||
|
frameWidth: 100
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.frame(width: vm.closedNotchSize.width)
|
||||||
|
|
||||||
|
// Right: Music Visualizer
|
||||||
|
visualizerView
|
||||||
|
}
|
||||||
|
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
|
||||||
|
.onHover { hovering in
|
||||||
|
isHovering = hovering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Album Art
|
||||||
|
|
||||||
|
private var albumArtView: some View {
|
||||||
|
HStack {
|
||||||
|
Color.clear
|
||||||
|
.aspectRatio(1, contentMode: .fit)
|
||||||
|
.background(
|
||||||
|
Image(nsImage: musicManager.albumArt)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
)
|
||||||
|
.clipped()
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 18)) // Dramatically increased corner radius for minimalistic mode
|
||||||
|
.frame(width: max(0, vm.effectiveClosedNotchHeight - 12), height: max(0, vm.effectiveClosedNotchHeight - 12))
|
||||||
|
}
|
||||||
|
.frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Visualizer
|
||||||
|
|
||||||
|
private var visualizerView: some View {
|
||||||
|
HStack {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor).gradient : Color.gray.gradient)
|
||||||
|
.frame(width: 50, alignment: .center)
|
||||||
|
.mask {
|
||||||
|
AudioSpectrumView(isPlaying: $musicManager.isPlaying)
|
||||||
|
.frame(width: 16, height: 12)
|
||||||
|
}
|
||||||
|
.frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)),
|
||||||
|
height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), alignment: .center)
|
||||||
|
}
|
||||||
|
.frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)),
|
||||||
|
height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), alignment: .center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,20 +20,22 @@ struct DynamicIslandHeader: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
HStack {
|
if !Defaults[.enableMinimalisticUI] {
|
||||||
if (!tvm.isEmpty || coordinator.alwaysShowTabs) && Defaults[.dynamicShelf] {
|
HStack {
|
||||||
TabSelectionView()
|
if (!tvm.isEmpty || coordinator.alwaysShowTabs) && Defaults[.dynamicShelf] {
|
||||||
} else if vm.notchState == .open {
|
TabSelectionView()
|
||||||
EmptyView()
|
} 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)
|
||||||
}
|
}
|
||||||
.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 {
|
if vm.notchState == .open && !Defaults[.enableMinimalisticUI] {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(NSScreen.screens
|
.fill(NSScreen.screens
|
||||||
.first(where: { $0.localizedName == coordinator.selectedScreen })?.safeAreaInsets.top ?? 0 > 0 ? .black : .clear)
|
.first(where: { $0.localizedName == coordinator.selectedScreen })?.safeAreaInsets.top ?? 0 > 0 ? .black : .clear)
|
||||||
|
|
@ -44,7 +46,7 @@ struct DynamicIslandHeader: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
if vm.notchState == .open {
|
if vm.notchState == .open && !Defaults[.enableMinimalisticUI] {
|
||||||
if Defaults[.showMirror] {
|
if Defaults[.showMirror] {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
vm.toggleCameraPreview()
|
vm.toggleCameraPreview()
|
||||||
|
|
|
||||||
275
DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift
Normal file
275
DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
//
|
||||||
|
// MinimalisticMusicPlayerView.swift
|
||||||
|
// DynamicIsland
|
||||||
|
//
|
||||||
|
// Created for minimalistic UI mode - Open state music player
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Defaults
|
||||||
|
|
||||||
|
struct MinimalisticMusicPlayerView: View {
|
||||||
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
|
let albumArtNamespace: Namespace.ID
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
// Header area with album art (matching DynamicIslandHeader height of 24pt)
|
||||||
|
HStack(alignment: .bottom, spacing: 10) {
|
||||||
|
MinimalisticAlbumArtView(vm: vm, albumArtNamespace: albumArtNamespace)
|
||||||
|
.frame(width: 50, height: 50)
|
||||||
|
|
||||||
|
// Song info aligned to bottom of album art
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
Text(MusicManager.shared.songTitle)
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Text(MusicManager.shared.artistName)
|
||||||
|
.font(.system(size: 10, weight: .regular))
|
||||||
|
.foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: MusicManager.shared.avgColor).ensureMinimumBrightness(factor: 0.6) : .gray)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Visualizer aligned to bottom
|
||||||
|
if useMusicVisualizer {
|
||||||
|
visualizer
|
||||||
|
.padding(.bottom, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 50) // Fixed height to accommodate album art
|
||||||
|
|
||||||
|
// Compact progress bar
|
||||||
|
progressBar
|
||||||
|
.padding(.top, 4)
|
||||||
|
|
||||||
|
// Compact playback controls
|
||||||
|
playbackControls
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.top, 0)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Visualizer
|
||||||
|
|
||||||
|
@Default(.useMusicVisualizer) var useMusicVisualizer
|
||||||
|
|
||||||
|
private var visualizer: some View {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Defaults[.coloredSpectrogram] ? Color(nsColor: MusicManager.shared.avgColor).gradient : Color.gray.gradient)
|
||||||
|
.mask {
|
||||||
|
AudioSpectrumView(isPlaying: .constant(MusicManager.shared.isPlaying))
|
||||||
|
.frame(width: 20, height: 16)
|
||||||
|
}
|
||||||
|
.frame(width: 20, height: 16)
|
||||||
|
.matchedGeometryEffect(id: "spectrum", in: albumArtNamespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Progress Bar (Full Width)
|
||||||
|
|
||||||
|
@ObservedObject var musicManager = MusicManager.shared
|
||||||
|
@State private var sliderValue: Double = 0
|
||||||
|
@State private var dragging: Bool = false
|
||||||
|
@State private var lastDragged: Date = .distantPast
|
||||||
|
|
||||||
|
private var progressBar: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
// Elapsed time - left
|
||||||
|
Text(formatTime(dragging ? sliderValue : musicManager.elapsedTime))
|
||||||
|
.font(.system(size: 11, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundColor(.white.opacity(0.7))
|
||||||
|
.frame(width: 42, alignment: .leading)
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
TimelineView(.animation(minimumInterval: 0.05)) { timeline in
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
// Background track
|
||||||
|
RoundedRectangle(cornerRadius: 3)
|
||||||
|
.fill(Color.white.opacity(0.2))
|
||||||
|
.frame(height: 6)
|
||||||
|
|
||||||
|
// Filled portion
|
||||||
|
RoundedRectangle(cornerRadius: 3)
|
||||||
|
.fill(sliderColor)
|
||||||
|
.frame(width: max(0, geometry.size.width * (currentSliderValue(timeline.date) / max(musicManager.songDuration, 1))), height: 6)
|
||||||
|
}
|
||||||
|
.gesture(
|
||||||
|
DragGesture(minimumDistance: 0)
|
||||||
|
.onChanged { value in
|
||||||
|
dragging = true
|
||||||
|
let newValue = min(max(0, Double(value.location.x / geometry.size.width) * musicManager.songDuration), musicManager.songDuration)
|
||||||
|
sliderValue = newValue
|
||||||
|
lastDragged = Date()
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
musicManager.seek(to: sliderValue)
|
||||||
|
dragging = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 6)
|
||||||
|
|
||||||
|
// Time remaining - right
|
||||||
|
Text("-\(formatTime(musicManager.songDuration - (dragging ? sliderValue : musicManager.elapsedTime)))")
|
||||||
|
.font(.system(size: 11, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundColor(.white.opacity(0.7))
|
||||||
|
.frame(width: 48, alignment: .trailing)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
sliderValue = musicManager.elapsedTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func currentSliderValue(_ date: Date) -> Double {
|
||||||
|
if dragging {
|
||||||
|
return sliderValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update slider value based on playback
|
||||||
|
if musicManager.isPlaying {
|
||||||
|
let timeSinceLastUpdate = date.timeIntervalSince(musicManager.lastUpdated)
|
||||||
|
let estimatedElapsed = musicManager.elapsedTime + (timeSinceLastUpdate * musicManager.playbackRate)
|
||||||
|
return min(estimatedElapsed, musicManager.songDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return musicManager.elapsedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sliderColor: Color {
|
||||||
|
switch Defaults[.sliderColor] {
|
||||||
|
case .white:
|
||||||
|
return .white
|
||||||
|
case .albumArt:
|
||||||
|
return Color(nsColor: musicManager.avgColor)
|
||||||
|
case .accent:
|
||||||
|
return .accentColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatTime(_ time: TimeInterval) -> String {
|
||||||
|
let minutes = Int(time) / 60
|
||||||
|
let seconds = Int(time) % 60
|
||||||
|
return String(format: "%d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Playback Controls (Larger)
|
||||||
|
|
||||||
|
private var playbackControls: some View {
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
if Defaults[.showShuffleAndRepeat] {
|
||||||
|
controlButton(icon: "shuffle", isActive: musicManager.isShuffled) {
|
||||||
|
Task { await musicManager.toggleShuffle() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controlButton(icon: "backward.fill", size: 18) {
|
||||||
|
Task { await musicManager.previousTrack() }
|
||||||
|
}
|
||||||
|
|
||||||
|
playPauseButton
|
||||||
|
|
||||||
|
controlButton(icon: "forward.fill", size: 18) {
|
||||||
|
Task { await musicManager.nextTrack() }
|
||||||
|
}
|
||||||
|
|
||||||
|
if Defaults[.showShuffleAndRepeat] {
|
||||||
|
controlButton(icon: repeatIcon, isActive: musicManager.repeatMode != .off) {
|
||||||
|
Task { await musicManager.toggleRepeat() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.padding(.top, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playPauseButton: some View {
|
||||||
|
Button(action: {
|
||||||
|
Task { await musicManager.togglePlay() }
|
||||||
|
}) {
|
||||||
|
Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill")
|
||||||
|
.font(.system(size: 24, weight: .semibold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(width: 50, height: 50)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func controlButton(icon: String, size: CGFloat = 18, isActive: Bool = false, action: @escaping () -> Void) -> some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.system(size: size, weight: .medium))
|
||||||
|
.foregroundColor(isActive ? .red : .white.opacity(0.8))
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
private var repeatIcon: String {
|
||||||
|
switch musicManager.repeatMode {
|
||||||
|
case .off: return "repeat"
|
||||||
|
case .all: return "repeat"
|
||||||
|
case .one: return "repeat.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Minimalistic Album Art
|
||||||
|
|
||||||
|
struct MinimalisticAlbumArtView: 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: 10))
|
||||||
|
.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: {
|
||||||
|
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: 10))
|
||||||
|
.matchedGeometryEffect(id: "albumArt", in: albumArtNamespace)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.opacity(musicManager.isPlaying ? 1 : 0.4)
|
||||||
|
.scaleEffect(musicManager.isPlaying ? 1 : 0.85)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -246,23 +246,29 @@ struct NotchHomeView: View {
|
||||||
|
|
||||||
private var mainContent: some View {
|
private var mainContent: some View {
|
||||||
HStack(alignment: .top, spacing: 20) {
|
HStack(alignment: .top, spacing: 20) {
|
||||||
MusicPlayerView(albumArtNamespace: albumArtNamespace)
|
if Defaults[.enableMinimalisticUI] {
|
||||||
|
// Minimalistic mode: Only show compact music player
|
||||||
if Defaults[.showCalendar] {
|
MinimalisticMusicPlayerView(albumArtNamespace: albumArtNamespace)
|
||||||
CalendarView()
|
} else {
|
||||||
.onHover { isHovering in
|
// Normal mode: Show full music player with optional calendar and webcam
|
||||||
vm.isHoveringCalendar = isHovering
|
MusicPlayerView(albumArtNamespace: albumArtNamespace)
|
||||||
}
|
|
||||||
.environmentObject(vm)
|
if Defaults[.showCalendar] {
|
||||||
}
|
CalendarView()
|
||||||
|
.onHover { isHovering in
|
||||||
if Defaults[.showMirror],
|
vm.isHoveringCalendar = isHovering
|
||||||
webcamManager.cameraAvailable,
|
}
|
||||||
vm.notchState == .open {
|
.environmentObject(vm)
|
||||||
CameraPreviewView(webcamManager: webcamManager)
|
}
|
||||||
.scaledToFit()
|
|
||||||
.opacity(vm.notchState == .closed ? 0 : 1)
|
if Defaults[.showMirror],
|
||||||
.blur(radius: vm.notchState == .closed ? 20 : 0)
|
webcamManager.cameraAvailable,
|
||||||
|
vm.notchState == .open {
|
||||||
|
CameraPreviewView(webcamManager: webcamManager)
|
||||||
|
.scaledToFit()
|
||||||
|
.opacity(vm.notchState == .closed ? 0 : 1)
|
||||||
|
.blur(radius: vm.notchState == .closed ? 20 : 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.transition(.opacity.animation(.smooth.speed(0.9))
|
.transition(.opacity.animation(.smooth.speed(0.9))
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import UniformTypeIdentifiers
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@StateObject var extensionManager = DynamicIslandExtensionManager()
|
@StateObject var extensionManager = DynamicIslandExtensionManager()
|
||||||
@State private var selectedTab = "General"
|
@State private var selectedTab = "General"
|
||||||
|
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||||
|
|
||||||
let updaterController: SPUStandardUpdaterController?
|
let updaterController: SPUStandardUpdaterController?
|
||||||
|
|
||||||
|
|
@ -38,8 +39,10 @@ struct SettingsView: View {
|
||||||
NavigationLink(value: "Media") {
|
NavigationLink(value: "Media") {
|
||||||
Label("Media", systemImage: "play.laptopcomputer")
|
Label("Media", systemImage: "play.laptopcomputer")
|
||||||
}
|
}
|
||||||
NavigationLink(value: "Calendar") {
|
if !enableMinimalisticUI {
|
||||||
Label("Calendar", systemImage: "calendar")
|
NavigationLink(value: "Calendar") {
|
||||||
|
Label("Calendar", systemImage: "calendar")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(value: "HUD") {
|
NavigationLink(value: "HUD") {
|
||||||
Label("HUDs", systemImage: "dial.medium.fill")
|
Label("HUDs", systemImage: "dial.medium.fill")
|
||||||
|
|
@ -47,20 +50,22 @@ struct SettingsView: View {
|
||||||
NavigationLink(value: "Battery") {
|
NavigationLink(value: "Battery") {
|
||||||
Label("Battery", systemImage: "battery.100.bolt")
|
Label("Battery", systemImage: "battery.100.bolt")
|
||||||
}
|
}
|
||||||
NavigationLink(value: "Timer") {
|
if !enableMinimalisticUI {
|
||||||
Label("Timer", systemImage: "timer")
|
NavigationLink(value: "Timer") {
|
||||||
}
|
Label("Timer", systemImage: "timer")
|
||||||
NavigationLink(value: "Stats") {
|
}
|
||||||
Label("Stats", systemImage: "chart.xyaxis.line")
|
NavigationLink(value: "Stats") {
|
||||||
}
|
Label("Stats", systemImage: "chart.xyaxis.line")
|
||||||
NavigationLink(value: "Clipboard") {
|
}
|
||||||
Label("Clipboard", systemImage: "clipboard")
|
NavigationLink(value: "Clipboard") {
|
||||||
}
|
Label("Clipboard", systemImage: "clipboard")
|
||||||
NavigationLink(value: "ScreenAssistant") {
|
}
|
||||||
Label("Screen Assistant", systemImage: "brain.head.profile")
|
NavigationLink(value: "ScreenAssistant") {
|
||||||
}
|
Label("Screen Assistant", systemImage: "brain.head.profile")
|
||||||
NavigationLink(value: "ColorPicker") {
|
}
|
||||||
Label("Color Picker", systemImage: "eyedropper")
|
NavigationLink(value: "ColorPicker") {
|
||||||
|
Label("Color Picker", systemImage: "eyedropper")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if extensionManager.installedExtensions
|
if extensionManager.installedExtensions
|
||||||
.contains(where: { $0.bundleIdentifier == downloadManagerExtension }) {
|
.contains(where: { $0.bundleIdentifier == downloadManagerExtension }) {
|
||||||
|
|
@ -68,8 +73,10 @@ struct SettingsView: View {
|
||||||
Label("Downloads", systemImage: "square.and.arrow.down")
|
Label("Downloads", systemImage: "square.and.arrow.down")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(value: "Shelf") {
|
if !enableMinimalisticUI {
|
||||||
Label("Shelf", systemImage: "books.vertical")
|
NavigationLink(value: "Shelf") {
|
||||||
|
Label("Shelf", systemImage: "books.vertical")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(value: "Shortcuts") {
|
NavigationLink(value: "Shortcuts") {
|
||||||
Label("Shortcuts", systemImage: "keyboard")
|
Label("Shortcuts", systemImage: "keyboard")
|
||||||
|
|
@ -165,9 +172,24 @@ struct GeneralSettings: View {
|
||||||
@Default(.openNotchOnHover) var openNotchOnHover
|
@Default(.openNotchOnHover) var openNotchOnHover
|
||||||
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
|
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
|
||||||
@Default(.showRecordingIndicator) var showRecordingIndicator
|
@Default(.showRecordingIndicator) var showRecordingIndicator
|
||||||
|
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
Section {
|
||||||
|
Defaults.Toggle("Enable Minimalistic UI", key: .enableMinimalisticUI)
|
||||||
|
.onChange(of: enableMinimalisticUI) { _, newValue in
|
||||||
|
if newValue {
|
||||||
|
// Auto-enable simpler animation mode
|
||||||
|
Defaults[.useModernCloseAnimation] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("UI Mode")
|
||||||
|
} footer: {
|
||||||
|
Text("Minimalistic mode focuses on media controls and system HUDs, hiding all extra features for a clean, focused experience. Automatically enables simpler animations.")
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Defaults.Toggle("Menubar icon", key: .menubarIcon)
|
Defaults.Toggle("Menubar icon", key: .menubarIcon)
|
||||||
LaunchAtLogin.Toggle("Launch at login")
|
LaunchAtLogin.Toggle("Launch at login")
|
||||||
|
|
@ -563,6 +585,7 @@ struct Media: View {
|
||||||
@Default(.hideNotchOption) var hideNotchOption
|
@Default(.hideNotchOption) var hideNotchOption
|
||||||
@Default(.enableSneakPeek) private var enableSneakPeek
|
@Default(.enableSneakPeek) private var enableSneakPeek
|
||||||
@Default(.sneakPeekStyles) var sneakPeekStyles
|
@Default(.sneakPeekStyles) var sneakPeekStyles
|
||||||
|
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
|
@ -616,7 +639,21 @@ struct Media: View {
|
||||||
ForEach(SneakPeekStyle.allCases) { style in
|
ForEach(SneakPeekStyle.allCases) { style in
|
||||||
Text(style.rawValue).tag(style)
|
Text(style.rawValue).tag(style)
|
||||||
}
|
}
|
||||||
}.disabled(!enableSneakPeek)
|
}
|
||||||
|
.disabled(!enableSneakPeek || enableMinimalisticUI)
|
||||||
|
.onChange(of: enableMinimalisticUI) { _, isMinimalistic in
|
||||||
|
// Force standard sneak peek style when minimalistic UI is enabled
|
||||||
|
if isMinimalistic {
|
||||||
|
sneakPeekStyles = .standard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if enableMinimalisticUI {
|
||||||
|
Text("Sneak peek style is locked to Standard in minimalistic mode")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Stepper(value: $waitInterval, in: 0...10, step: 1) {
|
Stepper(value: $waitInterval, in: 0...10, step: 1) {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
|
||||||
|
|
@ -349,6 +349,9 @@ extension Defaults.Keys {
|
||||||
// Polling removed - now uses event-driven private API detection (CGSIsScreenWatcherPresent)
|
// Polling removed - now uses event-driven private API detection (CGSIsScreenWatcherPresent)
|
||||||
// static let enableScreenRecordingPolling = Key<Bool>("enableScreenRecordingPolling", default: false)
|
// static let enableScreenRecordingPolling = Key<Bool>("enableScreenRecordingPolling", default: false)
|
||||||
|
|
||||||
|
// MARK: Minimalistic UI Mode
|
||||||
|
static let enableMinimalisticUI = Key<Bool>("enableMinimalisticUI", default: false)
|
||||||
|
|
||||||
// Helper to determine the default media controller based on macOS version
|
// Helper to determine the default media controller based on macOS version
|
||||||
static var defaultMediaController: MediaControllerType {
|
static var defaultMediaController: MediaControllerType {
|
||||||
if #available(macOS 15.4, *) {
|
if #available(macOS 15.4, *) {
|
||||||
|
|
|
||||||
|
|
@ -132,9 +132,12 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func calculateDynamicNotchSize() -> CGSize {
|
private func calculateDynamicNotchSize() -> CGSize {
|
||||||
|
// Use minimalistic size if minimalistic UI is enabled
|
||||||
|
let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
|
||||||
|
|
||||||
// Only apply dynamic sizing when on stats tab and stats are enabled
|
// Only apply dynamic sizing when on stats tab and stats are enabled
|
||||||
guard DynamicIslandViewCoordinator.shared.currentView == .stats && Defaults[.enableStatsFeature] else {
|
guard DynamicIslandViewCoordinator.shared.currentView == .stats && Defaults[.enableStatsFeature] else {
|
||||||
return openNotchSize
|
return baseSize
|
||||||
}
|
}
|
||||||
|
|
||||||
let enabledGraphsCount = [
|
let enabledGraphsCount = [
|
||||||
|
|
@ -148,10 +151,10 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
|
||||||
// If 4+ graphs are enabled, increase width
|
// If 4+ graphs are enabled, increase width
|
||||||
if enabledGraphsCount >= 4 {
|
if enabledGraphsCount >= 4 {
|
||||||
let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120
|
let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120
|
||||||
return CGSize(width: openNotchSize.width + extraWidth, height: openNotchSize.height)
|
return CGSize(width: baseSize.width + extraWidth, height: baseSize.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
return openNotchSize
|
return baseSize
|
||||||
}
|
}
|
||||||
|
|
||||||
func close() {
|
func close() {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ let downloadSneakSize: CGSize = .init(width: 65, height: 1)
|
||||||
let batterySneakSize: CGSize = .init(width: 160, height: 1)
|
let batterySneakSize: CGSize = .init(width: 160, height: 1)
|
||||||
|
|
||||||
let openNotchSize: CGSize = .init(width: 640, height: 190)
|
let openNotchSize: CGSize = .init(width: 640, height: 190)
|
||||||
|
let minimalisticOpenNotchSize: CGSize = .init(width: 420, height: 190)
|
||||||
let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14))
|
let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14))
|
||||||
|
let minimalisticCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 35, bottom: 35), closed: cornerRadiusInsets.closed)
|
||||||
|
|
||||||
enum MusicPlayerImageSizes {
|
enum MusicPlayerImageSizes {
|
||||||
static let cornerRadiusInset: (opened: CGFloat, closed: CGFloat) = (opened: 13.0, closed: 4.0)
|
static let cornerRadiusInset: (opened: CGFloat, closed: CGFloat) = (opened: 13.0, closed: 4.0)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue