diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 65aa46d..7d3e437 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -361,6 +361,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Migrate legacy progress bar settings Defaults.Keys.migrateProgressBarStyle() + Defaults.Keys.migrateMusicAuxControls() // Initialize idle animations (load bundled + built-in face) idleAnimationManager.initializeDefaultAnimations() diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 90670fb..ac86707 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -10497,6 +10497,10 @@ } } }, + "Left button" : { + "comment" : "A label for a picker that lets the user select a control for the left button on a media player.", + "isCommentAutoGenerated" : true + }, "License key" : { "localizations" : { "fr" : { @@ -15403,10 +15407,6 @@ } } }, - "Replace repeat button with media output control" : { - "comment" : "A toggle that allows users to replace the repeat button with a media output control.", - "isCommentAutoGenerated" : true - }, "Replaces system HUD notifications with Dynamic Island displays." : { }, @@ -15582,6 +15582,10 @@ } } }, + "Right button" : { + "comment" : "A label for the right button in the media controls picker.", + "isCommentAutoGenerated" : true + }, "Rotation:" : { "localizations" : { "fr" : { @@ -16791,6 +16795,10 @@ }, "Show floating pause/stop controls" : { + }, + "Show media output control in other layouts" : { + "comment" : "A toggle that lets the user enable or disable the media output control in layouts other than the home screen.", + "isCommentAutoGenerated" : true }, "Show on a specific display" : { "localizations" : { diff --git a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift index 32cc877..34aa282 100644 --- a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift +++ b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift @@ -17,7 +17,8 @@ import AppKit struct MinimalisticMusicPlayerView: View { @EnvironmentObject var vm: DynamicIslandViewModel let albumArtNamespace: Namespace.ID - @Default(.showMediaOutputControl) var showMediaOutputControl + @Default(.musicAuxLeftControl) private var leftAuxControl + @Default(.musicAuxRightControl) private var rightAuxControl @ObservedObject private var reminderManager = ReminderLiveActivityManager.shared @Default(.enableReminderLiveActivity) private var enableReminderLiveActivity @Default(.enableLyrics) private var enableLyrics @@ -601,9 +602,7 @@ private struct MinimalisticReminderDetailsView: View { private var playbackControls: some View { HStack(spacing: 16) { if Defaults[.showShuffleAndRepeat] { - controlButton(icon: "shuffle", isActive: musicManager.isShuffled) { - Task { await musicManager.toggleShuffle() } - } + auxButton(for: leftAuxControl) } controlButton(icon: "backward.fill", size: 18) { @@ -616,16 +615,8 @@ private struct MinimalisticReminderDetailsView: View { Task { await musicManager.nextTrack() } } - // (lyrics toggle removed from UI; controlled via Settings -> Media -> Enable Lyrics) - if Defaults[.showShuffleAndRepeat] { - if showMediaOutputControl { - MinimalisticMediaOutputButton() - } else { - controlButton(icon: repeatIcon, isActive: musicManager.repeatMode != .off) { - Task { await musicManager.toggleRepeat() } - } - } + auxButton(for: rightAuxControl) } } .frame(maxWidth: .infinity, alignment: .center) @@ -657,6 +648,26 @@ private struct MinimalisticReminderDetailsView: View { action: action ) } + + @ViewBuilder + private func auxButton(for control: MusicAuxiliaryControl) -> some View { + switch control { + case .shuffle: + controlButton(icon: "shuffle", isActive: musicManager.isShuffled) { + Task { await musicManager.toggleShuffle() } + } + case .repeatMode: + controlButton(icon: repeatIcon, isActive: musicManager.repeatMode != .off) { + Task { await musicManager.toggleRepeat() } + } + case .mediaOutput: + MinimalisticMediaOutputButton() + case .lyrics: + controlButton(icon: enableLyrics ? "quote.bubble.fill" : "quote.bubble", isActive: enableLyrics) { + enableLyrics.toggle() + } + } + } private struct MinimalisticMediaOutputButton: View { @ObservedObject private var routeManager = AudioRouteManager.shared @StateObject private var volumeModel = MediaOutputVolumeViewModel() diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index ad5859e..61a8568 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -120,7 +120,9 @@ struct MusicControlsView: View { @State private var dragging: Bool = false @State private var lastDragged: Date = .distantPast let showShuffleAndRepeat: Bool - @Default(.showMediaOutputControl) private var showMediaOutputControl + @Default(.musicAuxLeftControl) private var leftAuxControl + @Default(.musicAuxRightControl) private var rightAuxControl + @Default(.enableLyrics) private var enableLyrics var body: some View { VStack(alignment: .leading) { @@ -213,12 +215,7 @@ struct MusicControlsView: View { private var playbackControls: some View { HStack(spacing: 8) { if showShuffleAndRepeat { - HoverButton( - icon: "shuffle", iconColor: musicManager.isShuffled ? .red : .white, - scale: .medium - ) { - MusicManager.shared.toggleShuffle() - } + auxButton(for: leftAuxControl) } HoverButton(icon: "backward.fill", scale: .medium) { MusicManager.shared.previousTrack() @@ -230,13 +227,7 @@ struct MusicControlsView: View { MusicManager.shared.nextTrack() } if showShuffleAndRepeat { - if showMediaOutputControl { - MediaOutputPickerButton() - } else { - HoverButton(icon: repeatIcon, iconColor: repeatIconColor, scale: .medium) { - MusicManager.shared.toggleRepeat() - } - } + auxButton(for: rightAuxControl) } } .frame(maxWidth: .infinity, alignment: .center) @@ -261,6 +252,38 @@ struct MusicControlsView: View { return .red } } + + @ViewBuilder + private func auxButton(for control: MusicAuxiliaryControl) -> some View { + switch control { + case .shuffle: + HoverButton( + icon: "shuffle", + iconColor: musicManager.isShuffled ? .red : .white, + scale: .medium + ) { + MusicManager.shared.toggleShuffle() + } + case .repeatMode: + HoverButton( + icon: repeatIcon, + iconColor: repeatIconColor, + scale: .medium + ) { + MusicManager.shared.toggleRepeat() + } + case .mediaOutput: + MediaOutputPickerButton() + case .lyrics: + HoverButton( + icon: enableLyrics ? "quote.bubble.fill" : "quote.bubble", + iconColor: enableLyrics ? .accentColor : .white, + scale: .medium + ) { + enableLyrics.toggle() + } + } + } } // MARK: - Main View diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 8f3139b..ea4cf75 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -855,6 +855,8 @@ struct Media: View { @Default(.sneakPeekStyles) var sneakPeekStyles @Default(.enableMinimalisticUI) var enableMinimalisticUI @Default(.showShuffleAndRepeat) private var showShuffleAndRepeat + @Default(.musicAuxLeftControl) private var musicAuxLeftControl + @Default(.musicAuxRightControl) private var musicAuxRightControl var body: some View { Form { @@ -896,9 +898,21 @@ struct Media: View { } } Defaults.Toggle(key: .showMediaOutputControl) { - Text("Replace repeat button with media output control") + Text("Show media output control in other layouts") } .disabled(!showShuffleAndRepeat) + if showShuffleAndRepeat { + Picker("Left button", selection: $musicAuxLeftControl) { + ForEach(MusicAuxiliaryControl.allCases) { control in + Text(control.displayName).tag(control) + } + } + Picker("Right button", selection: $musicAuxRightControl) { + ForEach(MusicAuxiliaryControl.allCases) { control in + Text(control.displayName).tag(control) + } + } + } } header: { Text("Media controls") } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 843dc57..eeeec99 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -196,6 +196,28 @@ enum ProgressBarStyle: String, CaseIterable, Identifiable, Defaults.Serializable var id: String { self.rawValue } } +enum MusicAuxiliaryControl: String, CaseIterable, Identifiable, Defaults.Serializable { + case shuffle + case repeatMode + case mediaOutput + case lyrics + + var id: String { rawValue } + + var displayName: String { + switch self { + case .shuffle: + return "Shuffle" + case .repeatMode: + return "Repeat" + case .mediaOutput: + return "Media Output" + case .lyrics: + return "Lyrics Toggle" + } + } +} + enum TimerIconColorMode: String, CaseIterable, Identifiable, Defaults.Serializable { case adaptive = "Adaptive" case solid = "Solid" @@ -372,6 +394,9 @@ extension Defaults.Keys { static let waitInterval = Key("waitInterval", default: 3) static let showShuffleAndRepeat = Key("showShuffleAndRepeat", default: false) static let showMediaOutputControl = Key("showMediaOutputControl", default: false) + static let musicAuxLeftControl = Key("musicAuxLeftControl", default: .shuffle) + static let musicAuxRightControl = Key("musicAuxRightControl", default: .repeatMode) + static let didMigrateMusicAuxControls = Key("didMigrateMusicAuxControls", default: false) // Enable lock screen media widget (shows the standalone panel when screen is locked) static let enableLockScreenMediaWidget = Key("enableLockScreenMediaWidget", default: true) static let enableLockScreenWeatherWidget = Key("enableLockScreenWeatherWidget", default: true) @@ -552,4 +577,14 @@ extension Defaults.Keys { Defaults[.progressBarStyle] = .gradient } } + + static func migrateMusicAuxControls() { + guard Defaults[.didMigrateMusicAuxControls] == false else { return } + + if Defaults[.showMediaOutputControl] { + Defaults[.musicAuxRightControl] = .mediaOutput + } + + Defaults[.didMigrateMusicAuxControls] = true + } }