From 13e60c52111613228180e5fbd7c304d0aa013d0b Mon Sep 17 00:00:00 2001 From: delli Date: Sat, 15 Nov 2025 14:21:31 +0100 Subject: [PATCH] added lyrics feature --- .../components/MinimalisticMusicView.swift | 85 ++++-- .../Notch/MinimalisticMusicPlayerView.swift | 119 ++++++++- .../components/Notch/NotchHomeView.swift | 12 + .../components/Settings/SettingsView.swift | 1 + DynamicIsland/managers/MusicManager.swift | 245 +++++++++++++++++- DynamicIsland/models/Constants.swift | 3 + .../models/DynamicIslandViewModel.swift | 44 +++- DynamicIsland/sizing/matters.swift | 2 +- 8 files changed, 464 insertions(+), 47 deletions(-) diff --git a/DynamicIsland/components/MinimalisticMusicView.swift b/DynamicIsland/components/MinimalisticMusicView.swift index 22af312..38655ec 100644 --- a/DynamicIsland/components/MinimalisticMusicView.swift +++ b/DynamicIsland/components/MinimalisticMusicView.swift @@ -9,42 +9,73 @@ import SwiftUI import Defaults +// Note: lyrics display is inlined into the main minimalistic view below and is controlled by Defaults[.enableLyrics] + struct MinimalisticMusicView: View { @EnvironmentObject var vm: DynamicIslandViewModel @ObservedObject var musicManager = MusicManager.shared + @Default(.enableLyrics) var enableLyrics @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( - GeometryReader { geo in - HStack(alignment: .center) { - if !musicManager.songTitle.isEmpty { - MarqueeText( - $musicManager.songTitle, - font: .system(size: 12, weight: .semibold), - nsFont: .subheadline, - textColor: Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor) : Color.gray, - minDuration: 0.4, - frameWidth: max(0, geo.size.width - 8) - ) + VStack(spacing: 2) { + // Main content row + HStack(spacing: 0) { + // Left: Album Art + albumArtView + + // Middle: Song Title, Artist and Lyrics (lyrics shown under artist when enabled) + Rectangle() + .fill(.black) + .overlay( + GeometryReader { geo in + VStack(alignment: .center, spacing: 2) { + if !musicManager.songTitle.isEmpty { + MarqueeText( + $musicManager.songTitle, + font: .system(size: 12, weight: .semibold), + nsFont: .subheadline, + textColor: Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor) : Color.gray, + minDuration: 0.4, + frameWidth: max(0, geo.size.width - 8) + ) + } + + // Artist name + if !musicManager.artistName.isEmpty { + Text(musicManager.artistName) + .font(.system(size: 10, weight: .regular)) + .foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor).ensureMinimumBrightness(factor: 0.6) : .gray) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .center) + } + + // Lyrics under the author name (same font size as author) + if enableLyrics { + Text(musicManager.currentLyrics) + .font(.system(size: 10, weight: .regular)) + .foregroundColor(.white.opacity(0.8)) + .lineLimit(2) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, 4) + .transition(.opacity) + .animation(.easeInOut(duration: 0.3), value: musicManager.currentLyrics) + } } + .frame(width: geo.size.width, height: geo.size.height, alignment: .center) } - .frame(width: geo.size.width, height: geo.size.height, alignment: .center) - } - ) - .frame(width: vm.closedNotchSize.width) - - // Right: Music Visualizer - visualizerView + ) + .frame(width: vm.closedNotchSize.width) + + // Right: Music Visualizer + visualizerView + } + + // (lyrics are displayed inline under the artist name) } - .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) + // reserve extra height when lyrics are enabled + .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0) + (enableLyrics ? 22 : 0), alignment: .center) .onHover { hovering in isHovering = hovering } diff --git a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift index 7e00c6d..9b1dff8 100644 --- a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift +++ b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift @@ -12,12 +12,15 @@ import Defaults import AppKit #endif +// Lyrics are shown/hidden only via Defaults[.enableLyrics] in settings. Inline display is used in the player views. + struct MinimalisticMusicPlayerView: View { @EnvironmentObject var vm: DynamicIslandViewModel let albumArtNamespace: Namespace.ID @Default(.showMediaOutputControl) var showMediaOutputControl @ObservedObject private var reminderManager = ReminderLiveActivityManager.shared @Default(.enableReminderLiveActivity) private var enableReminderLiveActivity + @Default(.enableLyrics) private var enableLyrics var body: some View { VStack(spacing: 0) { @@ -46,6 +49,19 @@ struct MinimalisticMusicPlayerView: View { .font(.system(size: 10, weight: .regular)) .foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor).ensureMinimumBrightness(factor: 0.6) : .gray) .lineLimit(1) + + // Lyrics shown under the author name, same font size as author + if enableLyrics { + Text(musicManager.currentLyrics) + .font(.system(size: 10, weight: .regular)) + .foregroundColor(.white.opacity(0.8)) + .lineLimit(2) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + .transition(.opacity) + .animation(.easeInOut(duration: 0.3), value: musicManager.currentLyrics) + } } .frame(width: textWidth, alignment: .leading) @@ -65,12 +81,75 @@ struct MinimalisticMusicPlayerView: View { playbackControls .padding(.top, 4) + // Lyrics are displayed inline under the artist name (see header area) + reminderList } .padding(.horizontal, 12) .padding(.top, -15) - .padding(.bottom, shouldShowReminderList ? ReminderLiveActivityManager.listBottomPadding : ReminderLiveActivityManager.baselineMinimalisticBottomPadding) + .padding(.bottom, shouldShowReminderList ? ReminderLiveActivityManager.listBottomPadding : ReminderLiveActivityManager.baselineMinimalisticBottomPadding) .frame(maxWidth: .infinity) + .frame(height: calculateDynamicHeight(), alignment: .center) + .animation(.easeInOut(duration: 0.3), value: musicManager.currentLyrics) + } + + // MARK: - TypingLyricView + + struct TypingLyricView: View { + let text: String + let color: Color + let id: Int + let playbackRate: Double + let isPlaying: Bool + @State private var displayed: String = "" + @State private var lastText: String = "" + @State private var animationTask: Task? + + var body: some View { + Text(displayed) + .font(.system(size: 13, weight: .medium)) + .foregroundColor(color) + .lineLimit(2) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 4) + .padding(.top, 4) + .id(id) + .onChange(of: text) { _, newText in + animateTyping(newText) + } + .onChange(of: isPlaying) { _, playing in + if !playing { + animationTask?.cancel() + } else if displayed != text { + animateTyping(text) + } + } + .onAppear { + animateTyping(text) + } + .onDisappear { + animationTask?.cancel() + } + } + + private func animateTyping(_ newText: String) { + animationTask?.cancel() + displayed = "" + lastText = newText + let chars = Array(newText) + + animationTask = Task { + for (i, c) in chars.enumerated() { + if Task.isCancelled { return } + try? await Task.sleep(for: .milliseconds(Int(30 / max(playbackRate, 0.1)))) + if Task.isCancelled { return } + if lastText == newText { + displayed += String(c) + } + } + } + } } private var reminderEntries: [ReminderLiveActivityManager.ReminderEntry] { @@ -85,6 +164,32 @@ struct MinimalisticMusicPlayerView: View { ReminderLiveActivityManager.additionalHeight(forRowCount: reminderEntries.count) } + private func calculateDynamicHeight() -> CGFloat { + var height: CGFloat = 50 // Base height for header + + // Add progress bar height + height += 6 + 4 // progress bar + top padding + + // Add playback controls height + height += 40 + 2 // controls + top padding + + // Add lyrics height if enabled in settings (reserve space even while loading) + if Defaults[.enableLyrics] { + height += 20 + 4 // lyrics + top padding + } + + // Add reminder list height if showing + if shouldShowReminderList { + height += reminderListHeight + ReminderLiveActivityManager.listTopPadding + } + + // Add padding + height += 15 // top padding + height += shouldShowReminderList ? ReminderLiveActivityManager.listBottomPadding : ReminderLiveActivityManager.baselineMinimalisticBottomPadding + + return height + } + private var reminderList: some View { MinimalisticReminderEventListView(reminders: reminderEntries) .frame(maxWidth: .infinity, alignment: .leading) @@ -472,23 +577,25 @@ private struct MinimalisticReminderDetailsView: View { // MARK: - Playback Controls (Larger) private var playbackControls: some View { - HStack(spacing: 20) { + HStack(spacing: 16) { 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() } } - + + // (lyrics toggle removed from UI; controlled via Settings -> Media -> Enable Lyrics) + if Defaults[.showShuffleAndRepeat] { if showMediaOutputControl { MinimalisticMediaOutputButton() diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index 40eb0d8..c148f95 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -155,6 +155,18 @@ struct MusicControlsView: View { frameWidth: width ) .fontWeight(.medium) + // Lyrics shown under the author name (same font size as author) when enabled in settings + if Defaults[.enableLyrics] { + Text(musicManager.currentLyrics) + .font(.headline) + .foregroundColor(.white.opacity(0.8)) + .lineLimit(2) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 2) + .transition(.opacity) + .animation(.easeInOut(duration: 0.3), value: musicManager.currentLyrics) + } } } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 726a0d4..80b4a7c 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -908,6 +908,7 @@ struct Media: View { isOn: $coordinator.musicLiveActivityEnabled.animation() ) Toggle("Enable sneak peek", isOn: $enableSneakPeek) + Defaults.Toggle("Enable lyrics", key: .enableLyrics) Picker("Sneak Peek Style", selection: $sneakPeekStyles){ ForEach(SneakPeekStyle.allCases) { style in Text(style.rawValue).tag(style) diff --git a/DynamicIsland/managers/MusicManager.swift b/DynamicIsland/managers/MusicManager.swift index 0604315..f9d0906 100644 --- a/DynamicIsland/managers/MusicManager.swift +++ b/DynamicIsland/managers/MusicManager.swift @@ -7,8 +7,21 @@ import AppKit import Combine import Defaults +import Foundation import SwiftUI +// MARK: - Lyric Data Structures +struct LyricLine: Identifiable, Codable { + let id = UUID() + let timestamp: TimeInterval + let text: String + + init(timestamp: TimeInterval, text: String) { + self.timestamp = timestamp + self.text = text + } +} + let defaultImage: NSImage = .init( systemSymbolName: "heart.fill", accessibilityDescription: "Album Art" @@ -48,6 +61,15 @@ class MusicManager: ObservableObject { @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @Published var usingAppIconForArtwork: Bool = false + // MARK: - Lyrics Properties + @Published var currentLyrics: String = "" + @Published var syncedLyrics: [LyricLine] = [] + @Published var showLyrics: Bool = false + @Published var currentLyricIndex: Int = -1 + + // Task used to periodically sync displayed lyric with playback position + private var lyricSyncTask: Task? + private var artworkData: Data? = nil private var liveStreamUnknownDurationCount: Int = 0 @@ -212,13 +234,14 @@ class MusicManager: ObservableObject { } self.artworkData = state.artwork - if artworkChanged || state.artwork == nil { - // Update last artwork change values - self.lastArtworkTitle = state.title - self.lastArtworkArtist = state.artist - self.lastArtworkAlbum = state.album - self.lastArtworkBundleIdentifier = state.bundleIdentifier - } + // Update last artwork change values + self.lastArtworkTitle = state.title + self.lastArtworkArtist = state.artist + self.lastArtworkAlbum = state.album + self.lastArtworkBundleIdentifier = state.bundleIdentifier + + // Fetch lyrics for new track whenever content changes + self.fetchLyrics() // Only update sneak peek if there's actual content and something changed if !state.title.isEmpty && !state.artist.isEmpty && state.isPlaying { @@ -246,6 +269,8 @@ class MusicManager: ObservableObject { if timeChanged { self.elapsedTime = state.currentTime + // Update current lyric based on elapsed time + self.updateCurrentLyric(for: state.currentTime) } if durationChanged { @@ -270,6 +295,14 @@ class MusicManager: ObservableObject { updateLiveStreamState(with: state) self.timestampDate = state.lastUpdated + + // Manage lyric sync task based on playback/lyrics availability + if Defaults[.enableLyrics] && !self.syncedLyrics.isEmpty { + // Ensure syncing runs while lyrics are enabled + startLyricSync() + } else { + stopLyricSync() + } } private func triggerFlipAnimation() { @@ -509,6 +542,204 @@ class MusicManager: ObservableObject { } } } + + // MARK: - Lyrics Methods + func fetchLyrics() { + guard Defaults[.enableLyrics] else { return } + // If the lyrics panel is visible already, provide immediate feedback + if showLyrics { + Task { @MainActor in + self.currentLyrics = "Loading lyrics..." + self.syncedLyrics = [] + self.currentLyricIndex = -1 + } + } + + Task { + do { + let lyrics = try await fetchLyricsFromAPI(artist: artistName, title: songTitle) + await MainActor.run { + self.syncedLyrics = lyrics + self.currentLyricIndex = -1 + if !lyrics.isEmpty { + self.currentLyrics = lyrics[0].text + } else { + self.currentLyrics = "" + } + + // If lyrics are enabled, start syncing them to playback position + if Defaults[.enableLyrics] && !self.syncedLyrics.isEmpty { + self.startLyricSync() + } else if self.syncedLyrics.isEmpty { + self.stopLyricSync() + } + } + } catch { + print("Failed to fetch lyrics: \(error)") + await MainActor.run { + self.syncedLyrics = [] + self.currentLyrics = "" + self.currentLyricIndex = -1 + self.stopLyricSync() + } + } + } + } + + private func fetchLyricsFromAPI(artist: String, title: String) async throws -> [LyricLine] { + guard !artist.isEmpty, !title.isEmpty else { return [] } + + // Normalize input and percent-encode + let cleanArtist = artist.folding(options: .diacriticInsensitive, locale: .current) + let cleanTitle = title.folding(options: .diacriticInsensitive, locale: .current) + guard let encodedArtist = cleanArtist.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let encodedTitle = cleanTitle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + return [] + } + + // Use LRCLIB search endpoint which returns an array JSON with `plainLyrics` and/or `syncedLyrics`. + let urlString = "https://lrclib.net/api/search?track_name=\(encodedTitle)&artist_name=\(encodedArtist)" + guard let url = URL(string: urlString) else { return [] } + + let (data, response) = try await URLSession.shared.data(from: url) + if let http = response as? HTTPURLResponse, http.statusCode == 200 { + // Try parse as array JSON (preferred) + if let jsonArray = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]], + let first = jsonArray.first { + let plain = (first["plainLyrics"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let synced = (first["syncedLyrics"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + + if !synced.isEmpty { + return parseLRC(synced) + } else if !plain.isEmpty { + return [LyricLine(timestamp: 0, text: plain)] + } else { + return [] + } + } else { + // Fallback: try to decode as UTF8 and handle as LRC or plain text + if let lrcString = String(data: data, encoding: .utf8), !lrcString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + // If it contains a syncedLyrics key in an object, try that + if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let synced = json["syncedLyrics"] as? String { + return parseLRC(synced) + } + // Otherwise treat as plain lyrics blob + return [LyricLine(timestamp: 0, text: lrcString.trimmingCharacters(in: .whitespacesAndNewlines))] + } + return [] + } + } else { + return [] + } + } + + private func parseLRC(_ lrc: String) -> [LyricLine] { + let lines = lrc.components(separatedBy: .newlines) + var lyrics: [LyricLine] = [] + + // Accept patterns like [m:ss], [mm:ss], [mm:ss.xx] where centiseconds are optional + let pattern = "\\[(\\d{1,2}):(\\d{2})(?:\\.(\\d{1,2}))?\\]" + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return [] } + + for line in lines { + let nsLine = line as NSString + let fullRange = NSRange(location: 0, length: nsLine.length) + if let match = regex.firstMatch(in: line, options: [], range: fullRange) { + let minRange = match.range(at: 1) + let secRange = match.range(at: 2) + let centiRange = match.range(at: 3) + + let minStr = minRange.location != NSNotFound ? nsLine.substring(with: minRange) : "0" + let secStr = secRange.location != NSNotFound ? nsLine.substring(with: secRange) : "0" + let centiStr = (centiRange.location != NSNotFound) ? nsLine.substring(with: centiRange) : "0" + + let minutes = Double(minStr) ?? 0 + let seconds = Double(secStr) ?? 0 + let centis = Double(centiStr) ?? 0 + let timestamp = minutes * 60 + seconds + centis / 100.0 + + let textStart = match.range.location + match.range.length + if textStart <= nsLine.length { + let text = nsLine.substring(from: textStart).trimmingCharacters(in: .whitespaces) + if !text.isEmpty { + lyrics.append(LyricLine(timestamp: timestamp, text: text)) + } + } + } + } + + return lyrics.sorted(by: { $0.timestamp < $1.timestamp }) + } + + func updateCurrentLyric(for elapsedTime: TimeInterval) { + guard !syncedLyrics.isEmpty else { return } + + // Find the current lyric based on elapsed time + var newIndex = -1 + for (index, lyric) in syncedLyrics.enumerated() { + if elapsedTime >= lyric.timestamp { + newIndex = index + } else { + break + } + } + + if newIndex != currentLyricIndex { + currentLyricIndex = newIndex + if newIndex >= 0 && newIndex < syncedLyrics.count { + currentLyrics = syncedLyrics[newIndex].text + } + } + } + + // Start a background task that periodically updates the displayed lyric + private func startLyricSync() { + // If already running, keep it + if lyricSyncTask != nil { return } + + lyricSyncTask = Task { [weak self] in + guard let self = self else { return } + while !Task.isCancelled { + // Compute estimated playback position and update lyric + let position = self.estimatedPlaybackPosition() + await MainActor.run { + self.updateCurrentLyric(for: position) + } + + // Sleep ~300ms between updates + try? await Task.sleep(nanoseconds: 300_000_000) + } + } + } + + private func stopLyricSync() { + lyricSyncTask?.cancel() + lyricSyncTask = nil + } + + func toggleLyrics() { + // Toggle the UI state first so the views can react immediately. + showLyrics.toggle() + + // If lyrics are requested to be shown but we don't have any yet, + // show a loading placeholder and start fetching asynchronously. + if showLyrics && syncedLyrics.isEmpty { + // Provide immediate feedback so the UI can show a loading state. + currentLyrics = "Loading lyrics..." + + Task { + await fetchLyrics() + + // If fetch completed but no lyrics were found, show a friendly message. + await MainActor.run { + if self.syncedLyrics.isEmpty && self.currentLyrics.isEmpty { + self.currentLyrics = "No lyrics found" + } + } + } + } + } } // MARK: - Album Art Flip Helper diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 1564258..4039c46 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -528,6 +528,9 @@ extension Defaults.Keys { // MARK: Minimalistic UI Mode static let enableMinimalisticUI = Key("enableMinimalisticUI", default: false) + // MARK: Lyrics Feature + static let enableLyrics = Key("enableLyrics", default: true) + // Helper to determine the default media controller based on macOS version static var defaultMediaController: MediaControllerType { if #available(macOS 15.4, *) { diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift index c536240..803f3e0 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -83,7 +83,34 @@ class DynamicIslandViewModel: NSObject, ObservableObject { let updatedTarget = self.calculateDynamicNotchSize() guard self.notchState == .open else { return } guard self.notchSize != updatedTarget else { return } - self.notchSize = updatedTarget + withAnimation(.smooth) { + self.notchSize = updatedTarget + } + if let delegate = AppDelegate.shared { + delegate.ensureWindowSize(updatedTarget, animated: true, force: false) + } + } + .store(in: &cancellables) + + // Observe settings + lyrics changes to dynamically resize the notch + let enableLyricsPublisher = Defaults.publisher(.enableLyrics).map { $0.newValue } + + enableLyricsPublisher + .combineLatest(MusicManager.shared.$currentLyrics) + .removeDuplicates { $0.0 == $1.0 && $0.1 == $1.1 } + .receive(on: RunLoop.main) + .sink { [weak self] _ in + guard let self else { return } + guard Defaults[.enableMinimalisticUI] else { return } + let updatedTarget = self.calculateDynamicNotchSize() + guard self.notchState == .open else { return } + guard self.notchSize != updatedTarget else { return } + withAnimation(.smooth) { + self.notchSize = updatedTarget + } + if let delegate = AppDelegate.shared { + delegate.ensureWindowSize(updatedTarget, animated: true, force: false) + } } .store(in: &cancellables) } @@ -171,27 +198,32 @@ class DynamicIslandViewModel: NSObject, ObservableObject { if Defaults[.enableMinimalisticUI] { let extraHeight = ReminderLiveActivityManager.additionalHeight(forRowCount: minimalisticReminderRowCount) baseSize.height += extraHeight + + // Add extra height for lyrics if enabled in settings + if Defaults[.enableLyrics] { + baseSize.height += 24 // Additional height for lyrics display + } } - + // Only apply dynamic sizing when on stats tab and stats are enabled guard DynamicIslandViewCoordinator.shared.currentView == .stats && Defaults[.enableStatsFeature] else { return baseSize } - + let enabledGraphsCount = [ Defaults[.showCpuGraph], - Defaults[.showMemoryGraph], + Defaults[.showMemoryGraph], Defaults[.showGpuGraph], Defaults[.showNetworkGraph], Defaults[.showDiskGraph] ].filter { $0 }.count - + // If 4+ graphs are enabled, increase width if enabledGraphsCount >= 4 { let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120 return CGSize(width: baseSize.width + extraWidth, height: baseSize.height) } - + return baseSize } diff --git a/DynamicIsland/sizing/matters.swift b/DynamicIsland/sizing/matters.swift index 2eff7fe..5a70013 100644 --- a/DynamicIsland/sizing/matters.swift +++ b/DynamicIsland/sizing/matters.swift @@ -15,7 +15,7 @@ let downloadSneakSize: CGSize = .init(width: 65, height: 1) let batterySneakSize: CGSize = .init(width: 160, height: 1) let openNotchSize: CGSize = .init(width: 640, height: 190) -let minimalisticOpenNotchSize: CGSize = .init(width: 420, height: 180) +let minimalisticOpenNotchSize: CGSize = .init(width: 420, height: 200) 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)